/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
test/perf/OpenFile-perf-files/brackets-concat.js
85 734 строки
3 MB
Jeonghyunsu
Correct typo (Peformance -> Performance) (#13902)
10 янв 2018, 23:47
10 янв 2018, 23:47
a74095f
Код
Авторство
О чём код?
/* ============================================================ * bootstrap-dropdown.js v1.4.0 * http://twitter.github.com/bootstrap/javascript.html#dropdown * ============================================================ * Copyright 2011 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function( $ ){ "use strict" /* DROPDOWN PLUGIN DEFINITION * ========================== */ $.fn.dropdown = function ( selector ) { return this.each(function () { $(this).delegate(selector || d, 'click', function (e) { var li = $(this).parent('li') , isActive = li.hasClass('open') clearMenus() !isActive && li.toggleClass('open') return false }) }) } /* APPLY TO STANDARD DROPDOWN ELEMENTS * =================================== */ var d = 'a.menu, .dropdown-toggle' function clearMenus() { $(d).parent('li').removeClass('open') } $(function () { $('html').bind("click", clearMenus) $('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' ) }) }( window.jQuery || window.ender ); define("widgets/bootstrap-dropdown", function(){}); /* ========================================================= * bootstrap-modal.js v1.4.0 * http://twitter.github.com/bootstrap/javascript.html#modal * ========================================================= * Copyright 2011 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ !function( $ ){ "use strict" /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) * ======================================================= */ var transitionEnd $(document).ready(function () { $.support.transition = (function () { var thisBody = document.body || document.documentElement , thisStyle = thisBody.style , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined return support })() // set CSS transition event type if ( $.support.transition ) { transitionEnd = "TransitionEnd" if ( $.browser.webkit ) { transitionEnd = "webkitTransitionEnd" } else if ( $.browser.mozilla ) { transitionEnd = "transitionend" } else if ( $.browser.opera ) { transitionEnd = "oTransitionEnd" } } }) /* MODAL PUBLIC CLASS DEFINITION * ============================= */ var Modal = function ( content, options ) { this.settings = $.extend({}, $.fn.modal.defaults, options) this.$element = $(content) .delegate('.close', 'click.modal', $.proxy(this.hide, this)) if ( this.settings.show ) { this.show() } return this } Modal.prototype = { toggle: function () { return this[!this.isShown ? 'show' : 'hide']() } , show: function () { var that = this this.isShown = true this.$element.trigger('show') escape.call(this) backdrop.call(this, function () { var transition = $.support.transition && that.$element.hasClass('fade') that.$element .appendTo(document.body) .show() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') transition ? that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) : that.$element.trigger('shown') }) return this } , hide: function (e) { e && e.preventDefault() if ( !this.isShown ) { return this } var that = this this.isShown = false escape.call(this) this.$element .trigger('hide') .removeClass('in') $.support.transition && this.$element.hasClass('fade') ? hideWithTransition.call(this) : hideModal.call(this) return this } } /* MODAL PRIVATE METHODS * ===================== */ function hideWithTransition() { // firefox drops transitionEnd events :{o var that = this , timeout = setTimeout(function () { that.$element.unbind(transitionEnd) hideModal.call(that) }, 500) this.$element.one(transitionEnd, function () { clearTimeout(timeout) hideModal.call(that) }) } function hideModal (that) { this.$element .hide() .trigger('hidden') backdrop.call(this) } function backdrop ( callback ) { var that = this , animate = this.$element.hasClass('fade') ? 'fade' : '' if ( this.isShown && this.settings.backdrop ) { var doAnimate = $.support.transition && animate this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') .appendTo(document.body) if ( this.settings.backdrop != 'static' ) { this.$backdrop.click($.proxy(this.hide, this)) } if ( doAnimate ) { this.$backdrop[0].offsetWidth // force reflow } this.$backdrop.addClass('in') doAnimate ? this.$backdrop.one(transitionEnd, callback) : callback() } else if ( !this.isShown && this.$backdrop ) { this.$backdrop.removeClass('in') $.support.transition && this.$element.hasClass('fade')? this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) : removeBackdrop.call(this) } else if ( callback ) { callback() } } function removeBackdrop() { this.$backdrop.remove() this.$backdrop = null } function escape() { var that = this if ( this.isShown && this.settings.keyboard ) { $(document).bind('keyup.modal', function ( e ) { if ( e.which == 27 ) { that.hide() } }) } else if ( !this.isShown ) { $(document).unbind('keyup.modal') } } /* MODAL PLUGIN DEFINITION * ======================= */ $.fn.modal = function ( options ) { var modal = this.data('modal') if (!modal) { if (typeof options == 'string') { options = { show: /show|toggle/.test(options) } } return this.each(function () { $(this).data('modal', new Modal(this, options)) }) } if ( options === true ) { return modal } if ( typeof options == 'string' ) { modal[options]() } else if ( modal ) { modal.toggle() } return this } $.fn.modal.Modal = Modal $.fn.modal.defaults = { backdrop: false , keyboard: false , show: false } /* MODAL DATA- IMPLEMENTATION * ========================== */ $(document).ready(function () { $('body').delegate('[data-controls-modal]', 'click', function (e) { e.preventDefault() var $this = $(this).data('show', true) $('#' + $this.attr('data-controls-modal')).modal( $this.data() ) }) }) }( window.jQuery || window.ender ); define("widgets/bootstrap-modal", function(){}); // path-utils.js - version 0.1 // Copyright (c) 2011, Kin Blas // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the <organization> nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. (function(l){function m(a){return function(e){return b.parseUrl(e)[a]}}var b={urlParseRE:/^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#\.]*(?:\.[^\?#\.]+)*(\.[^\?#\.]+)|[^\?#]*)))?(\?[^#]+)?)(#.*)?/,parsedUrlPropNames:"href,hrefNoHash,hrefNoSearch,domain,protocol,doubleSlash,authority,userinfo,username,password,host,hostname,port,pathname,directory,filename,filenameExtension,search,hash".split(","), defaultPorts:{http:"80",https:"443",ftp:"20",ftps:"990"},parseUrl:function(a){if(a&&typeof a==="object")return a;var a=b.urlParseRE.exec(a||"")||[],e=b.parsedUrlPropNames,c=e.length,f={},d;for(d=0;d<c;d++)f[e[d]]=a[d]||"";return f},port:function(a){a=b.parseUrl(a);return a.port||b.defaultPorts[a.protocol]},isSameDomain:function(a,e){return b.parseUrl(a).domain===b.parseUrl(e).domain},isRelativeUrl:function(a){return b.parseUrl(a).protocol===""},isAbsoluteUrl:function(a){return b.parseUrl(a).protocol!== ""},makePathAbsolute:function(a,e){if(a&&a.charAt(0)==="/")return a;for(var a=a||"",c=(e=e?e.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"")?e.split("/"):[],b=a.split("/"),d=0;d<b.length;d++){var j=b[d];switch(j){case ".":break;case "..":c.length&&c.pop();break;default:c.push(j)}}return"/"+c.join("/")},makePathRelative:function(a,b){for(var b=b?b.replace(/^\/|\/?[^\/]*$/g,""):"",a=a?a.replace(/^\//,""):"",c=b?b.split("/"):[],f=a.split("/"),d=[],j=c.length,g=false,i=0;i<j;i++)(g=g||f[0]!==c[i])?d.push(".."): f.shift();return d.concat(f).join("/")},makeUrlAbsolute:function(a,e){if(!b.isRelativeUrl(a))return a;var c=b.parseUrl(a),f=b.parseUrl(e),d=c.protocol||f.protocol,g=c.protocol?c.doubleSlash:c.doubleSlash||f.doubleSlash,h=c.authority||f.authority,i=c.pathname!=="",k=b.makePathAbsolute(c.pathname||f.filename,f.pathname);return d+g+h+k+(c.search||!i&&f.search||"")+c.hash}},g,h,k=b.parsedUrlPropNames,n=k.length;for(g=0;g<n;g++)h=k[g],b[h]||(b[h]=m(h));l.PathUtils=b})(window); define("thirdparty/path-utils/path-utils.min", function(){}); /** * Smart Auto Complete plugin * * Copyright (c) 2011 Lakshan Perera (laktek.com) * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) licenses. * */ /* Requirements: jQuery 1.5 or above Usage: $(target).smartAutoComplete({options}) Options: minCharLimit: (integer) minimum characters user have to type before invoking the autocomplete (default: 1) maxCharLimit: (integer) maximum characters user can type while invoking the autocomplete (default: null (unlimited)) maxResults: (integer) maximum number of results to return (default: null (unlimited)) delay: (integer) delay before autocomplete starts (default: 0) disabled: (boolean) whether autocomplete disabled on the field (default: false) forceSelect: (boolean) If set to true, field will be always filled with best matching result, without leaving the custom input. Better to enable this option, if you want autocomplete field to behave similar to a HTML select field. (Check Example 2 in the demo) (default: false) typeAhead: (boolean) If set to true, it will offer the best matching result in grey within the field; that can be auto-completed by pressing the right arrow-key or enter. This is similar to behaviour in Google Instant Search's query field (Check Example 3 in the demo) (default: false) source: (string/array) you can supply an array with items or a string containing a URL to fetch items for the source this is optional if you prefer to have your own filter method filter: (function) define a custom function that would return matching items to the entered text (this will override the default filtering algorithm) should return an array or a Deferred object (ajax call) parameters available: term, source resultFormatter: (function) the function you supply here will be called to format the output of an individual result. should return a string parameters available: result resultsContainer: (selector) to which element(s) the result should be appended. resultElement: (selector) references to the result elements collection (e.g. li, div.result) Events: keyIn: fires when user types into the field (parameters: query) resultsReady: fires when the filter function returns (parameters: results) showResults: fires when results are shown (parameters: results) noResults: fires when filter returns an empty array itemSelect: fires when user selects an item from the result list (paramters: item) itemFocus: fires when user highlights an item with mouse or arrow keys (paramters: item) itemUnfocus: fires when user moves out from an highlighted item (paramters: item) lostFocus: fires when autocomplete field loses focus by user clicking outside of the field or focusing on another field. Also, this event is fired when a value is selected }) */ (function($){ $.fn.smartAutoComplete = function(){ if(arguments.length < 1){ // get the smart autocomplete object of the first element and return var first_element = this[0]; return $(first_element).data("smart-autocomplete") } var default_filter_matcher = function(term, source, context){ var matcher = new RegExp(term.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), "i" ); return $.grep(source, function(value) { return matcher.test( value ); }); } var default_options = { minCharLimit: 1, maxCharLimit: null, maxResults: null, delay: 0, disabled: false, forceSelect: false, typeAhead: false, resultElement: "li", resultFormatter: function(r){ return ("<li>" + r + "</li>"); }, filter: function(term, source){ var context = this; var options = $(context).data('smart-autocomplete'); //when source is an array if($.type(source) === "array") { // directly map var results = default_filter_matcher(term, source, context); return results; } //when source is a string else if($.type(source) === "string"){ // treat the string as a URL endpoint // pass the query as 'term' return $.Deferred(function(dfd){ $.ajax({ url: source, data: {"term": term}, dataType: "json" }).success( function(data){ dfd.resolve( default_filter_matcher(term, data, context) ); }); }).promise(); } }, alignResultsContainer: false, clearResults: function(){ //remove type ahead field var type_ahead_field = $(this.context).prev(".smart_autocomplete_type_ahead_field"); $(this.context).css({ background: type_ahead_field.css("background") }); type_ahead_field.remove(); //clear results div $(this.resultsContainer).html(""); }, setCurrentSelectionToContext: function(){ if(this.rawResults.length > 0 && this.currentSelection >= 0) $(this.context).val(this.rawResults[(this.currentSelection)]); }, setItemSelected: function(val){ this.itemSelected = val; }, autocompleteFocused: false, setAutocompleteFocused: function(val){ this.autocompleteFocused = val; } }; //define the default events $.event.special.keyIn = { setup: function(){ return false; }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var source = options.source || null; var filter = options.filter; var maxChars = (options.maxCharLimit > 0 ? options.maxCharLimit : Number.POSITIVE_INFINITY) //event specific data var query = ev.smartAutocompleteData.query; if(options.disabled || (query.length > maxChars)){ return false; } //set item selected property options.setItemSelected(false); //set autocomplete focused options.setAutocompleteFocused(true); //call the filter function with delay setTimeout(function(){ $.when( filter.apply(options, [query, options.source]) ).done(function( results ){ //do the trimming var trimmed_results = (options.maxResults > 0 ? results.splice(0, options.maxResults) : results); $(context).trigger('resultsReady', [trimmed_results]); }); }, options.delay); } }; $.event.special.resultsReady = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //event specific data var results = ev.smartAutocompleteData.results; //exit if smart complete is disabled if(options.disabled) return false; //clear all previous results $(context).smartAutoComplete().clearResults(); //save the raw results options.rawResults = results; //fire the no match event and exit if no matching results if(results.length < 1){ $(context).trigger('noResults'); return false } //call the results formatter function var formatted_results = $.map(results, function(result){ return options.resultFormatter.apply(options, [result]); }); var formatted_results_html = formatted_results.join(""); //append the results to the container if(options.resultsContainer) $(options.resultsContainer).append(formatted_results_html); //trigger results ready event $(context).trigger('showResults', [results]); } }; $.event.special.showResults = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var results_container = $(options.resultsContainer); //event specific data var raw_results = ev.smartAutocompleteData.results; //type ahead if(options.typeAhead && (raw_results[0].substr(0, $(context).val().length) == $(context).val()) ){ var suggestion = raw_results[0]; //options.typeAheadExtractor($(context).val(), raw_results[0]); //add new typeAhead field $(context).before("<input class='smart_autocomplete_type_ahead_field' type='text' autocomplete='off' disabled='disabled' value='" + suggestion + "'/>"); $(context).css({ position: "relative", zIndex: 2, background: 'transparent' }); var typeAheadField = $(context).prev("input"); typeAheadField.css({ position: "absolute", zIndex: 1, overflow: 'hidden', background: $(context).css("background"), borderColor: 'transparent', width: $(context).width(), color: 'silver' }); //trigger item over for first item options.currentSelection = 0; if(results_container) $(context).trigger('itemFocus', results_container.children()[options.currentSelection]); } //show the results container after aligning it with the field if(results_container){ if(options.alignResultsContainer){ results_container.css({ position: "absolute", top: function(){ return $(context).offset().top + $(context).height(); }, left: function(){ return $(context).offset().left; }, width: function(){ return $(context).width(); }, zIndex: 1000 }) } results_container.show(); } } }; $.event.special.noResults = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var result_container = $(options.resultsContainer); if(result_container){ //clear previous results options.clearResults(); } } }; $.event.special.itemSelect = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //event specific data var selected_item = ev.smartAutocompleteData.item; //get the text from selected item var selected_value = $(selected_item).text() || $(selected_item).val(); //set it as the value of the autocomplete field $(context).val(selected_value); //set item selected property options.setItemSelected(true); //set number of current chars in field options.originalCharCount = $(context).val().length; //trigger lost focus $(context).trigger('lostFocus'); } }; $.event.special.itemFocus = { setup: function(){ return false }, _default: function(ev){ //event specific data var item = ev.smartAutocompleteData.item; $(item).addClass("smart_autocomplete_highlight"); } }; $.event.special.itemUnfocus = { setup: function(){ return false }, _default: function(ev){ //event specific data var item = ev.smartAutocompleteData.item; $(item).removeClass("smart_autocomplete_highlight"); } } $.event.special.lostFocus = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //if force select is selected and no item is selected, clear currently entered text if(options.forceSelect && !options.itemSelected) $(options.context).val(""); //unset autocomplete focused options.setAutocompleteFocused(false); //clear results options.clearResults(); //hide the results container if(options.resultsContainer) $(options.resultsContainer).hide(); //set current selection to null options.currentSelection = null; } }; var passed_options = arguments[0]; return this.each(function(i) { //set the options var options = $.extend(default_options, $(this).data("smart-autocomplete"), passed_options); //set the context options['context'] = this; //if a result container is not defined if($.type(options.resultsContainer) === 'undefined' ){ //define the default result container if it is already not defined var default_container = $("<ul class='smart_autocomplete_container' style='display:none'></ul>"); default_container.appendTo("body"); options.resultsContainer = default_container; options.alignResultsContainer = true; } $(this).data("smart-autocomplete", options); // bind user events $(this).keyup(function(ev){ //get the options var options = $(this).data("smart-autocomplete"); //up arrow if(ev.keyCode == '38'){ if(options.resultsContainer){ var current_selection = options.currentSelection || 0; var result_suggestions = $(options.resultsContainer).children(); if(current_selection >= 0) $(options.context).trigger('itemUnfocus', result_suggestions[current_selection] ); if(--current_selection <= 0) current_selection = 0; options['currentSelection'] = current_selection; $(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] ); } } //down arrow else if(ev.keyCode == '40'){ if(options.resultsContainer && options.resultsContainer.is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); if(current_selection >= 0) $(options.context).trigger('itemUnfocus', result_suggestions[current_selection] ); if(isNaN(current_selection) || null == current_selection || (++current_selection >= result_suggestions.length) ) current_selection = 0; options['currentSelection'] = current_selection; $(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] ); } //trigger keyIn event on down key else { $(options.context).trigger('keyIn', [$(this).val()]); } } //right arrow & enter key else if(ev.keyCode == '39' || ev.keyCode == '13'){ var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field'); if(options.resultsContainer && $(options.resultsContainer).is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); $(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] ); } else if(options.typeAhead && type_ahead_field.is(':visible')) $(options.context).trigger('itemSelect', [ type_ahead_field ] ); return false; } else { var current_char_count = $(options.context).val().length; //check whether the string has modified if(options.originalCharCount == current_char_count) return; //check minimum and maximum number of characters are typed if(current_char_count >= options.minCharLimit){ $(options.context).trigger('keyIn', [$(this).val()]); } else{ if(options.autocompleteFocused){ options.currentSelection = null; $(options.context).trigger('lostFocus'); } } } }); $(this).focus(function(){ //if the field is in a form capture the return key event $(this).closest("form").bind("keydown.block_for_smart_autocomplete", function(ev){ var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field'); if(ev.keyCode == '13'){ if(options.resultsContainer && $(options.resultsContainer).is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); $(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] ); return false; } else if(options.typeAhead && type_ahead_field.is(':visible') ){ $(options.context).trigger('itemSelect', [ type_ahead_field ] ); return false; } } }); if(options.forceSelect){ $(this).select(); } }); //check for loosing focus on smart complete field and results container //$(this).blur(function(ev){ $(document).bind("focusin click", function(ev){ if(options.autocompleteFocused){ var elemIsParent = $.contains(options.resultsContainer[0], ev.target); if(ev.target == options.resultsContainer[0] || ev.target == options.context || elemIsParent) return $(options.context).closest("form").unbind("keydown.block_for_smart_autocomplete"); $(options.context).trigger('lostFocus'); } }); //bind events to results container $(options.resultsContainer).delegate(options.resultElement, 'mouseenter.smart_autocomplete', function(){ var current_selection = options.currentSelection || 0; var result_suggestions = $(options.resultsContainer).children(); options['currentSelection'] = $(this).prevAll().length; $(options.context).trigger('itemFocus', [this] ); }); $(options.resultsContainer).delegate(options.resultElement, 'mouseleave.smart_autocomplete', function(){ $(options.context).trigger('itemUnfocus', [this] ); }); $(options.resultsContainer).delegate(options.resultElement, 'click.smart_autocomplete', function(){ $(options.context).trigger('itemSelect', [this]); return false }); //bind plugin specific events $(this).bind({ keyIn: function(ev, query){ ev.smartAutocompleteData = {'query': query }; }, resultsReady: function(ev, results){ ev.smartAutocompleteData = {'results': results }; }, showResults: function(ev, results){ ev.smartAutocompleteData = {'results': results } }, noResults: function(){}, lostFocus: function(){}, itemSelect: function(ev, item){ ev.smartAutocompleteData = {'item': item }; }, itemFocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; }, itemUnfocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; } }); }); } })(jQuery); define("thirdparty/smart-auto-complete/jquery.smart_autocomplete", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ /** * Utilities for working with Deferred, Promise, and other asynchronous processes. */ define('utils/Async',['require','exports','module'],function (require, exports, module) { // Further ideas for Async utilities... // - Utilities for blocking UI until a Promise completes? // - A "SuperDeferred" could feature some very useful enhancements: // - API for cancellation (non guaranteed, best attempt) // - Easier way to add a timeout clause (withTimeout() wrapper below is more verbose) // - Encapsulate the task kickoff code so you can start it later, e.g. superDeferred.start() // - Deferred/Promise are unable to do anything akin to a 'finally' block. It'd be nice if we // could harvest exceptions across all steps of an async process and pipe them to a handler, // so that we don't leave UI-blocking overlays up forever, etc. But this is hard: we'd have // wrap every async callback (including low-level native ones that don't use [Super]Deferred) // to catch exceptions, and then understand which Deferred(s) the code *would* have resolved/ // rejected had it run to completion. /** * Executes a series of tasks in parallel, returning a "master" Promise that is resolved once * all the tasks have resolved. If one or more tasks fail, behavior depends on the failFast * flag: * - If true, the master Promise is rejected as soon as the first task fails. The remaining * tasks continue to completion in the background. * - If false, the master Promise is rejected after all tasks have completed. * * If nothing fails: (M = master promise; 1-4 = tasks; d = done; F = fail) * M ------------d * 1 >---d . * 2 >------d . * 3 >---------d . * 4 >------------d * * With failFast = false: * M ------------F * 1 >---d . . * 2 >------d . . * 3 >---------F . * 4 >------------d * * With failFast = true: -- equivalent to $.when() * M ---------F * 1 >---d . * 2 >------d . * 3 >---------F * 4 >------------d (#4 continues even though master Promise has failed) * (Note: if tasks finish synchronously, the behavior is more like failFast=false because you * won't get a chance to respond to the master Promise until after all items have been processed) * * To perform task-specific work after an individual task completes, attach handlers to each * Promise before beginProcessItem() returns it. * * Note: don't use this if individual tasks (or their done/fail handlers) could ever show a user- * visible dialog: because they run in parallel, you could show multiple dialogs atop each other. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @param {!boolean} failFast * @return {$.Promise} */ function doInParallel(items, beginProcessItem, failFast) { var promises = []; var masterDeferred = new $.Deferred(); if (items.length === 0) { masterDeferred.resolve(); } else { var numCompleted = 0; var hasFailed = false; items.forEach(function (item, i) { var itemPromise = beginProcessItem(item, i); promises.push(itemPromise); itemPromise.fail(function () { if (failFast) { masterDeferred.reject(); } else { hasFailed = true; } }); itemPromise.always(function () { numCompleted++; if (numCompleted === items.length) { if (hasFailed) { masterDeferred.reject(); } else { masterDeferred.resolve(); } } }); }); } return masterDeferred.promise(); } /** * Executes a series of tasks in serial (task N does not begin until task N-1 has completed). * Returns a "master" Promise that is resolved once all the tasks have resolved. If one or more * tasks fail, behavior depends on the failAndStopFast flag: * - If true, the master Promise is rejected as soon as the first task fails. The remaining * tasks are never started (the serial sequence is stopped). * - If false, the master Promise is rejected after all tasks have completed. * * If nothing fails: * M ------------d * 1 >---d . * 2 >--d . * 3 >--d . * 4 >--d * * With failAndStopFast = false: * M ------------F * 1 >---d . . * 2 >--d . . * 3 >--F . * 4 >--d * * With failAndStopFast = true: * M ---------F * 1 >---d . * 2 >--d . * 3 >--F * 4 (#4 never runs) * * To perform task-specific work after an individual task completes, attach handlers to each * Promise before beginProcessItem() returns it. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @param {!boolean} failAndStopFast * @return {$.Promise} */ function doSequentially(items, beginProcessItem, failAndStopFast) { var masterDeferred = new $.Deferred(), hasFailed = false; function doItem(i) { if (i >= items.length) { if (hasFailed) { masterDeferred.reject(); } else { masterDeferred.resolve(); } return; } var itemPromise = beginProcessItem(items[i], i); itemPromise.done(function () { doItem(i + 1); }); itemPromise.fail(function () { if (failAndStopFast) { masterDeferred.reject(); // note: we do NOT process any further items in this case } else { hasFailed = true; doItem(i + 1); } }); } doItem(0); return masterDeferred.promise(); } /** * Executes a series of tasks sequentially in time-slices less than maxBlockingTime. * Processing yields by idleTime between time-slices. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} fnProcessItem * @param {!number} maxBlockingTime * @param {!number} idleTime * @return {$.Promise} */ function doSequentiallyInBackground(items, fnProcessItem, maxBlockingTime, idleTime) { maxBlockingTime = maxBlockingTime || 15; idleTime = idleTime || 30; var sliceStartTime = (new Date()).getTime(); return doSequentially(items, function (item, i) { var result = new $.Deferred(); // process the next item fnProcessItem(item, i); // if we've exhausted our maxBlockingTime if ((new Date()).getTime() - sliceStartTime >= maxBlockingTime) { //yield window.setTimeout(function () { sliceStartTime = (new Date()).getTime(); result.resolve(); }, idleTime); } else { //continue processing result.resolve(); } return result; }, false); } /** * Executes a series of tasks in parallel, saving up error info from any that fail along the way. * Returns a Promise that is only resolved/rejected once all tasks are complete. This is * essentially a wrapper around doInParallel(..., false). * * If one or more tasks failed, the entire "master" promise is rejected at the end - with one * argument: an array objects, one per failed task. Each error object contains: * - item -- the entry in items whose task failed * - error -- the first argument passed to the fail() handler when the task failed * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @return {$.Promise} */ function doInParallel_aggregateErrors(items, beginProcessItem) { var errors = []; var masterDeferred = new $.Deferred(); var parallelResult = doInParallel( items, function (item, i) { var itemResult = beginProcessItem(item, i); itemResult.fail(function (error) { errors.push({ item: item, error: error }); }); return itemResult; }, false ); parallelResult .done(function () { masterDeferred.resolve(); }) .fail(function () { masterDeferred.reject(errors); }); return masterDeferred.promise(); } /** Value passed to fail() handlers that have been triggered due to withTimeout()'s timeout */ var ERROR_TIMEOUT = {}; /** * Adds timeout-driven failure to a Promise: returns a new Promise that is resolved/rejected when * the given original Promise is resolved/rejected, OR is rejected after the given delay - whichever * happens first. * * If the original Promise is resolved/rejected first, done()/fail() handlers receive arguments * piped from the original Promise. If the timeout occurs first instead, fail() is called with the * token Async.ERROR_TIMEOUT. * * @param {$.Promise} promise * @param {number} timeout * @return {$.Promise} */ function withTimeout(promise, timeout) { var wrapper = new $.Deferred(); var timer = window.setTimeout(function () { wrapper.reject(ERROR_TIMEOUT); }, timeout); promise.always(function () { window.clearTimeout(timer); }); // If the wrapper was already rejected due to timeout, the Promise's calls to resolve/reject // won't do anything promise.pipe(wrapper.resolve, wrapper.reject); return wrapper.promise(); } // Define public API exports.doInParallel = doInParallel; exports.doSequentially = doSequentially; exports.doSequentiallyInBackground = doSequentiallyInBackground; exports.doInParallel_aggregateErrors = doInParallel_aggregateErrors; exports.withTimeout = withTimeout; exports.ERROR_TIMEOUT = ERROR_TIMEOUT; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50*/ /*global $, define, brackets, FileError, InvalidateStateError */ define('file/NativeFileSystem',['require','exports','module','utils/Async'],function (require, exports, module) { var Async = require("utils/Async"); /* * Generally NativeFileSystem mimics the File API working draft * http://www.w3.org/TR/file-system-api/. The w3 entry point * requestFileSystem is replaced with our own requestNativeFileSystem. * * The current implementation is incomplete and noteably does not * support the Blob data type and synchronous APIs. DirectoryEntry * and FileEntry read/write capabilities are mostly implemented, but * delete is not. File writing is limited to UTF-8 text. */ var NativeFileSystem = { /** * Amount of time we wait for async calls to return (in milliseconds) * Not all async calls are wrapped with something that times out and * calls the error callback. Timeouts are not specified in the W3C spec. * @const * @type {number} */ ASYNC_TIMEOUT: 2000, /** showOpenDialog * * @param {bool} allowMultipleSelection * @param {bool} chooseDirectories * @param {string} title * @param {string} initialPath * @param {Array.<string>} fileTypes * @param {function(...)} successCallback * @param {function(...)} errorCallback * @constructor */ showOpenDialog: function (allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, successCallback, errorCallback) { if (!successCallback) { return; } var files = brackets.fs.showOpenDialog( allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, function (err, data) { if (!err) { successCallback(data); } else if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } } ); }, /** requestNativeFileSystem * * @param {string} path * @param {function(...)} successCallback * @param {function(...)} errorCallback */ requestNativeFileSystem: function (path, successCallback, errorCallback) { brackets.fs.stat(path, function (err, data) { if (!err) { // FIXME (issue #247): return a NativeFileSystem object var root = new NativeFileSystem.DirectoryEntry(path); successCallback(root); } else if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }, _nativeToFileError: function (nativeErr) { var error; switch (nativeErr) { // We map ERR_UNKNOWN and ERR_INVALID_PARAMS to SECURITY_ERR, // since there aren't specific mappings for these. case brackets.fs.ERR_UNKNOWN: case brackets.fs.ERR_INVALID_PARAMS: error = FileError.SECURITY_ERR; break; case brackets.fs.ERR_NOT_FOUND: error = FileError.NOT_FOUND_ERR; break; case brackets.fs.ERR_CANT_READ: error = FileError.NOT_READABLE_ERR; break; // It might seem like you should use FileError.ENCODING_ERR for this, // but according to the spec that's for malformed URLs. case brackets.fs.ERR_UNSUPPORTED_ENCODING: error = FileError.SECURITY_ERR; break; case brackets.fs.ERR_CANT_WRITE: error = FileError.NO_MODIFICATION_ALLOWED_ERR; break; case brackets.fs.ERR_OUT_OF_SPACE: error = FileError.QUOTA_EXCEEDED_ERR; break; case brackets.fs.PATH_EXISTS_ERR: error = FileError.PATH_EXISTS_ERR; break; default: // The HTML file spec says SECURITY_ERR is a catch-all to be used in situations // not covered by other error codes. error = FileError.SECURITY_ERR; } return new NativeFileSystem.FileError(error); } }; /** class: Encodings * * Static class that contains constants for file * encoding types. */ NativeFileSystem.Encodings = {}; NativeFileSystem.Encodings.UTF8 = "UTF-8"; NativeFileSystem.Encodings.UTF16 = "UTF-16"; /** class: _FSEncodings * * Internal static class that contains constants for file * encoding types to be used by internal file system * implimentation. */ NativeFileSystem._FSEncodings = {}; NativeFileSystem._FSEncodings.UTF8 = "utf8"; NativeFileSystem._FSEncodings.UTF16 = "utf16"; /** * Converts an IANA encoding name to internal encoding name. * http://www.iana.org/assignments/character-sets * * @param {String} encoding The IANA encoding string. */ NativeFileSystem.Encodings._IANAToFS = function (encoding) { //IANA names are case-insensitive encoding = encoding.toUpperCase(); switch (encoding) { case (NativeFileSystem.Encodings.UTF8): return NativeFileSystem._FSEncodings.UTF8; case (NativeFileSystem.Encodings.UTF16): return NativeFileSystem._FSEncodings.UTF16; default: return undefined; } }; var Encodings = NativeFileSystem.Encodings; var _FSEncodings = NativeFileSystem._FSEncodings; /** class: Entry * * @param {string} name * @param {string} isFile * @constructor */ NativeFileSystem.Entry = function (fullPath, isDirectory) { this.isDirectory = isDirectory; this.isFile = !isDirectory; if (fullPath) { // add trailing "/" to directory paths if (isDirectory && (fullPath.charAt(fullPath.length - 1) !== "/")) { fullPath = fullPath.concat("/"); } } this.fullPath = fullPath; this.name = null; // default if extraction fails if (fullPath) { var pathParts = fullPath.split("/"); // Extract name from the end of the fullPath (account for trailing slash(es)) while (!this.name && pathParts.length) { this.name = pathParts.pop(); } } // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-filesystem this.filesystem = null; }; NativeFileSystem.Entry.prototype.moveTo = function (parent, newName, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-moveTo }; NativeFileSystem.Entry.prototype.copyTo = function (parent, newName, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-copyTo }; NativeFileSystem.Entry.prototype.toURL = function (mimeType) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-toURL }; NativeFileSystem.Entry.prototype.remove = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-remove }; NativeFileSystem.Entry.prototype.getParent = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-remove }; NativeFileSystem.Entry.prototype.getMetadata = function (successCallBack, errorCallback) { brackets.fs.stat(this.fullPath, function (err, stat) { if (err === brackets.fs.NO_ERROR) { var metadata = new NativeFileSystem.Metadata(stat.mtime); successCallBack(metadata); } else { errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }; /** * Stores information about a FileEntry */ NativeFileSystem.Metadata = function (modificationTime) { // modificationTime is read only this.modificationTime = modificationTime; }; /** class: FileEntry * This interface represents a file on a file system. * * @param {string} name * @constructor * @extends {Entry} */ NativeFileSystem.FileEntry = function (name) { NativeFileSystem.Entry.call(this, name, false); }; NativeFileSystem.FileEntry.prototype = new NativeFileSystem.Entry(); NativeFileSystem.FileEntry.prototype.toString = function () { return "[FileEntry " + this.fullPath + "]"; }; /** * Creates a new FileWriter associated with the file that this FileEntry represents. * * @param {function (FileWriter)} successCallback * @param {function (FileError)} errorCallback */ NativeFileSystem.FileEntry.prototype.createWriter = function (successCallback, errorCallback) { var fileEntry = this; // [NoInterfaceObject] // interface FileWriter : FileSaver var FileWriter = function (data) { NativeFileSystem.FileSaver.call(this, data); // FileWriter private memeber vars this._length = 0; this._position = 0; }; FileWriter.prototype.length = function () { return this._length; }; FileWriter.prototype.position = function () { return this._position; }; // TODO (issue #241): handle Blob data instead of string FileWriter.prototype.write = function (data) { if (data === null || data === undefined) { throw new Error(); } if (this.readyState === NativeFileSystem.FileSaver.WRITING) { throw new NativeFileSystem.FileException(NativeFileSystem.FileException.INVALID_STATE_ERR); } this._readyState = NativeFileSystem.FileSaver.WRITING; if (this.onwritestart) { // TODO (issue #241): progressevent this.onwritestart(); } var self = this; brackets.fs.writeFile(fileEntry.fullPath, data, _FSEncodings.UTF8, function (err) { if ((err !== brackets.fs.NO_ERROR) && self.onerror) { var fileError = NativeFileSystem._nativeToFileError(err); // TODO (issue #241): set readonly FileSaver.error attribute // self._error = fileError; self.onerror(fileError); // TODO (issue #241): partial write, update length and position } // else { // TODO (issue #241): After changing data argument to Blob, use // Blob.size to update position and length upon successful // completion of a write. // self.position = ; // self.length = ; // } // DONE is set regardless of error self._readyState = NativeFileSystem.FileSaver.DONE; if (self.onwrite) { // TODO (issue #241): progressevent self.onwrite(); } if (self.onwriteend) { // TODO (issue #241): progressevent self.onwriteend(); } }); }; FileWriter.prototype.seek = function (offset) { }; FileWriter.prototype.truncate = function (size) { }; var fileWriter = new FileWriter(); // initialize file length var result = new $.Deferred(); brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { // Ignore "file not found" errors. It's okay if the file doesn't exist yet. if (err !== brackets.fs.ERR_NOT_FOUND) { fileWriter._err = err; } if (contents) { fileWriter._length = contents.length; } result.resolve(); }); result.done(function () { if (fileWriter._err && (errorCallback !== undefined)) { errorCallback(NativeFileSystem._nativeToFileError(fileWriter._err)); } else if (successCallback !== undefined) { successCallback(fileWriter); } }); }; /** * Obtains the File objecte for a FileEntry object * * @param {function(...)} successCallback * @param {function(...)} errorCallback */ NativeFileSystem.FileEntry.prototype.file = function (successCallback, errorCallback) { var newFile = new NativeFileSystem.File(this); successCallback(newFile); // TODO (issue #241): errorCallback }; /** * This interface extends the FileException interface described in to add * several new error codes. Any errors that need to be reported synchronously, * including all that occur during use of the synchronous filesystem methods, * are reported using the FileException exception. * * @param {number} code The code attribute, on getting, must return one of the * constants of the FileException exception, which must be the most appropriate * code from the table below. */ NativeFileSystem.FileException = function (code) { this.code = code || 0; }; // FileException constants Object.defineProperties( NativeFileSystem.FileException, { NOT_FOUND_ERR: { value: 1, writable: false }, SECURITY_ERR: { value: 2, writable: false }, ABORT_ERR: { value: 3, writable: false }, NOT_READABLE_ERR: { value: 4, writable: false }, ENCODING_ERR: { value: 5, writable: false }, NO_MODIFICATION_ALLOWED_ERR: { value: 6, writable: false }, INVALID_STATE_ERR: { value: 7, writable: false }, SYNTAX_ERR: { value: 8, writable: false }, QUOTA_EXCEEDED_ERR: { value: 10, writable: false } } ); /** * This interface provides methods to monitor the asynchronous writing of blobs * to disk using progress events and event handler attributes. * * This interface is specified to be used within the context of the global * object (Window) and within Web Workers. * * @param {Blob} data * @constructor */ NativeFileSystem.FileSaver = function (data) { // FileSaver private member vars this._data = data; this._readyState = NativeFileSystem.FileSaver.INIT; this._error = null; }; // FileSaver constants Object.defineProperties( NativeFileSystem.FileSaver, { INIT: { value: 1, writable: false }, WRITING: { value: 2, writable: false }, DONE: { value: 3, writable: false } } ); // FileSaver methods /** * */ NativeFileSystem.FileSaver.prototype.readyState = function () { return this._readyState; }; // TODO (issue #241): http://dev.w3.org/2009/dap/file-system/file-writer.html#widl-FileSaver-abort-void NativeFileSystem.FileSaver.prototype.abort = function () { // If readyState is DONE or INIT, terminate this overall series of steps without doing anything else.. if (this._readyState === NativeFileSystem.FileSaver.INIT || this._readyState === NativeFileSystem.FileSaver.DONE) { return; } // TODO (issue #241): Terminate any steps having to do with writing a file. // Set the error attribute to a FileError object with the code ABORT_ERR. this._error = new NativeFileSystem.FileError(FileError.ABORT_ERR); // Set readyState to DONE. this._readyState = NativeFileSystem.FileSaver.DONE; /* TODO (issue #241): Dispatch a progress event called abort Dispatch a progress event called writeend Stop dispatching any further progress events. Terminate this overall set of steps. */ }; /** * This interface represents a directory on a file system. * * @constructor * @param {string} name * @extends {Entry} */ NativeFileSystem.DirectoryEntry = function (name) { NativeFileSystem.Entry.call(this, name, true); // TODO (issue #241): void removeRecursively (VoidCallback successCallback, optional ErrorCallback errorCallback); }; NativeFileSystem.DirectoryEntry.prototype = new NativeFileSystem.Entry(); NativeFileSystem.DirectoryEntry.prototype.toString = function () { return "[DirectoryEntry " + this.fullPath + "]"; }; NativeFileSystem.DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-getDirectory }; NativeFileSystem.DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-removeRecursively }; NativeFileSystem.DirectoryEntry.prototype.createReader = function () { var dirReader = new NativeFileSystem.DirectoryReader(); dirReader._directory = this; return dirReader; }; /** * Creates or looks up a file. * * @param {string} path Either an absolute path or a relative path from this * DirectoryEntry to the file to be looked up or created. It is an error * to attempt to create a file whose immediate parent does not yet * exist. * @param {Object.<string, boolean>} options * @param {function (number)} successCallback * @param {function (number)} errorCallback */ NativeFileSystem.DirectoryEntry.prototype.getFile = function (path, options, successCallback, errorCallback) { var fileFullPath = path; function isRelativePath(path) { // If the path contains a colons it must be a full path on Windows (colons are // not valid path characters on mac or in URIs) if (path.indexOf(":") !== -1) { return false; } // For everyone else, absolute paths start with a "/" return path[0] !== "/"; } // resolve relative paths relative to the DirectoryEntry if (isRelativePath(path)) { fileFullPath = this.fullPath + path; } var createFileEntry = function () { if (successCallback) { successCallback(new NativeFileSystem.FileEntry(fileFullPath)); } }; var createFileError = function (err) { if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } }; // Use stat() to check if file exists brackets.fs.stat(fileFullPath, function (err, stats) { if ((err === brackets.fs.NO_ERROR)) { // NO_ERROR implies the path already exists // throw error if the file the path is a directory if (stats.isDirectory()) { if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.TYPE_MISMATCH_ERR)); } return; } // throw error if the file exists but create is exclusive if (options.create && options.exclusive) { if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.PATH_EXISTS_ERR)); } return; } // Create a file entry for the existing file. If create == true, // a file entry is created without error. createFileEntry(); } else if (err === brackets.fs.ERR_NOT_FOUND) { // ERR_NOT_FOUND implies we write a new, empty file // create the file if (options.create) { brackets.fs.writeFile(fileFullPath, "", _FSEncodings.UTF8, function (err) { if (err) { createFileError(err); } else { createFileEntry(); } }); return; } // throw error if file not found and the create == false if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.NOT_FOUND_ERR)); } } else { // all other brackets.fs.stat() errors createFileError(err); } }); }; /** class: DirectoryReader */ NativeFileSystem.DirectoryReader = function () { }; /** readEntries * * @param {function(...)} successCallback * @param {function(...)} errorCallback * @returns {Array.<Entry>} */ NativeFileSystem.DirectoryReader.prototype.readEntries = function (successCallback, errorCallback) { var rootPath = this._directory.fullPath; brackets.fs.readdir(rootPath, function (err, filelist) { if (!err) { var entries = []; var lastError = null; // stat() to determine type of each entry, then populare entries array with objects var masterPromise = Async.doInParallel(filelist, function (filename, index) { var deferred = new $.Deferred(); var itemFullPath = rootPath + filelist[index]; brackets.fs.stat(itemFullPath, function (statErr, statData) { if (!statErr) { if (statData.isDirectory()) { entries[index] = new NativeFileSystem.DirectoryEntry(itemFullPath); } else if (statData.isFile()) { entries[index] = new NativeFileSystem.FileEntry(itemFullPath); } else { entries[index] = null; // neither a file nor a dir, so don't include it } deferred.resolve(); } else { lastError = NativeFileSystem._nativeToFileError(statErr); deferred.reject(lastError); } }); return deferred.promise(); }, true); // We want the error callback to get called after some timeout (in case some deferreds don't return). // So, we need to wrap masterPromise in another deferred that has this timeout functionality var timeoutWrapper = Async.withTimeout(masterPromise, NativeFileSystem.ASYNC_TIMEOUT); // Add the callbacks to this top-level Promise, which wraps all the individual deferred objects timeoutWrapper.then( function () { // success // The entries array may have null values if stat returned things that were // neither a file nor a dir. So, we need to clean those out. var cleanedEntries = [], i; for (i = 0; i < entries.length; i++) { if (entries[i]) { cleanedEntries.push(entries[i]); } } successCallback(cleanedEntries); }, function (err) { // error if (err === Async.ERROR_TIMEOUT) { // SECURITY_ERR is the HTML5 File catch-all error, and there isn't anything // more fitting for a timeout. err = new NativeFileSystem.FileError(FileError.SECURITY_ERR); } else { err = lastError; } if (errorCallback) { errorCallback(err); } } ); } else { // There was an error reading the initial directory. errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }; /** class: FileReader * * @extends {EventTarget} */ NativeFileSystem.FileReader = function () { // TODO (issue #241): this classes should extend EventTarget // states this.EMPTY = 0; this.LOADING = 1; this.DONE = 2; // readyState is read only this.readyState = this.EMPTY; // File or Blob data // TODO (issue #241): readonly attribute any result; // TODO (issue #241): readonly attribute DOMError error; // event handler attributes this.onloadstart = null; this.onprogress = null; this.onload = null; this.onabort = null; this.onerror = null; this.onloadend = null; }; // TODO (issue #241): extend EventTarget (draft status, not implememnted in webkit) // NativeFileSystem.FileReader.prototype = new NativeFileSystem.EventTarget() NativeFileSystem.FileReader.prototype.readAsArrayBuffer = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsArrayBuffer }; NativeFileSystem.FileReader.prototype.readAsBinaryString = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsBinaryStringAsync }; NativeFileSystem.FileReader.prototype.readAsDataURL = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsDataURL }; NativeFileSystem.FileReader.prototype.abort = function () { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-abort }; /** readAsText * * @param {Blob} blob * @param {string} encoding (IANA Encoding Name) */ NativeFileSystem.FileReader.prototype.readAsText = function (blob, encoding) { var self = this; if (!encoding) { encoding = Encodings.UTF8; } var internalEncoding = Encodings._IANAToFS(encoding); if (this.readyState === this.LOADING) { throw new InvalidateStateError(); } this.readyState = this.LOADING; if (this.onloadstart) { this.onloadstart(); // TODO (issue #241): progressevent } brackets.fs.readFile(blob._fullPath, internalEncoding, function (err, data) { // TODO (issue #241): the event objects passed to these event handlers is fake and incomplete right now var fakeEvent = { loaded: 0, total: 0 }; // The target for this event is the FileReader and the data/err result is stored in the FileReader fakeEvent.target = self; self.result = data; self.error = NativeFileSystem._nativeToFileError(err); if (err) { self.readyState = self.DONE; if (self.onerror) { self.onerror(fakeEvent); } } else { self.readyState = self.DONE; // TODO (issue #241): this should be the file/blob size, but we don't have code to get that yet, so for know assume a file size of 1 // and since we read the file in one go, assume 100% after the first read fakeEvent.loaded = 1; fakeEvent.total = 1; if (self.onprogress) { self.onprogress(fakeEvent); } // TODO (issue #241): onabort not currently supported since our native implementation doesn't support it // if (self.onabort) // self.onabort(fakeEvent); if (self.onload) { self.onload(fakeEvent); } if (self.onloadend) { self.onloadend(); } } }); }; /** class: Blob * * @constructor * param {Entry} entry */ NativeFileSystem.Blob = function (fullPath) { this._fullPath = fullPath; // TODO (issue #241): implement, readonly this.size = 0; // TODO (issue #241): implement, readonly this.type = null; }; NativeFileSystem.Blob.prototype.slice = function (start, end, contentType) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-slice }; /** class: File * * @constructor * param {Entry} entry * @extends {Blob} */ NativeFileSystem.File = function (entry) { NativeFileSystem.Blob.call(this, entry.fullPath); // TODO (issue #241): implement, readonly this.name = ""; // TODO (issue #241): implement, readonly this.lastModifiedDate = null; }; /** class: FileError * * Implementation of HTML file API error code return class. Note that we don't * actually define the error codes here--we rely on the browser's built-in FileError * class's constants. In other words, external clients of this API should always * use FileError.<constant-name>, not NativeFileSystem.FileError.<constant-name>. * * @constructor * @param {number} code The error code to return with this FileError. Must be * one of the codes defined in the FileError class. */ NativeFileSystem.FileError = function (code) { this.code = code || 0; }; // Define public API exports.NativeFileSystem = NativeFileSystem; }); /* * jsTree 1.0-rc3 * http://jstree.com/ * * Copyright (c) 2010 Ivan Bozhanov (vakata.com) * * Licensed same as jquery - under the terms of either the MIT License or the GPL Version 2 License * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * $Date: 2011-02-09 01:17:14 +0200 (ср, 09 февр 2011) $ * $Revision: 236 $ */ /*jslint browser: true, onevar: true, undef: true, bitwise: true, strict: true */ /*global window : false, clearInterval: false, clearTimeout: false, document: false, setInterval: false, setTimeout: false, jQuery: false, navigator: false, XSLTProcessor: false, DOMParser: false, XMLSerializer: false*/ // top wrapper to prevent multiple inclusion (is this OK?) (function () { if(jQuery && jQuery.jstree) { return; } var is_ie6 = false, is_ie7 = false, is_ff2 = false; /* * jsTree core */ (function ($) { // Common functions not related to jsTree // decided to move them to a `vakata` "namespace" $.vakata = {}; // CSS related functions $.vakata.css = { get_css : function(rule_name, delete_flag, sheet) { rule_name = rule_name.toLowerCase(); var css_rules = sheet.cssRules || sheet.rules, j = 0; do { if(css_rules.length && j > css_rules.length + 5) { return false; } if(css_rules[j].selectorText && css_rules[j].selectorText.toLowerCase() == rule_name) { if(delete_flag === true) { if(sheet.removeRule) { sheet.removeRule(j); } if(sheet.deleteRule) { sheet.deleteRule(j); } return true; } else { return css_rules[j]; } } } while (css_rules[++j]); return false; }, add_css : function(rule_name, sheet) { if($.jstree.css.get_css(rule_name, false, sheet)) { return false; } if(sheet.insertRule) { sheet.insertRule(rule_name + ' { }', 0); } else { sheet.addRule(rule_name, null, 0); } return $.vakata.css.get_css(rule_name); }, remove_css : function(rule_name, sheet) { return $.vakata.css.get_css(rule_name, true, sheet); }, add_sheet : function(opts) { var tmp = false, is_new = true; if(opts.str) { if(opts.title) { tmp = $("style[id='" + opts.title + "-stylesheet']")[0]; } if(tmp) { is_new = false; } else { tmp = document.createElement("style"); tmp.setAttribute('type',"text/css"); if(opts.title) { tmp.setAttribute("id", opts.title + "-stylesheet"); } } if(tmp.styleSheet) { if(is_new) { document.getElementsByTagName("head")[0].appendChild(tmp); tmp.styleSheet.cssText = opts.str; } else { tmp.styleSheet.cssText = tmp.styleSheet.cssText + " " + opts.str; } } else { tmp.appendChild(document.createTextNode(opts.str)); document.getElementsByTagName("head")[0].appendChild(tmp); } return tmp.sheet || tmp.styleSheet; } if(opts.url) { if(document.createStyleSheet) { try { tmp = document.createStyleSheet(opts.url); } catch (e) { } } else { tmp = document.createElement('link'); tmp.rel = 'stylesheet'; tmp.type = 'text/css'; tmp.media = "all"; tmp.href = opts.url; document.getElementsByTagName("head")[0].appendChild(tmp); return tmp.styleSheet; } } } }; // private variables var instances = [], // instance array (used by $.jstree.reference/create/focused) focused_instance = -1, // the index in the instance array of the currently focused instance plugins = {}, // list of included plugins prepared_move = {}; // for the move_node function // jQuery plugin wrapper (thanks to jquery UI widget function) $.fn.jstree = function (settings) { var isMethodCall = (typeof settings == 'string'), // is this a method call like $().jstree("open_node") args = Array.prototype.slice.call(arguments, 1), returnValue = this; // if a method call execute the method on all selected instances if(isMethodCall) { if(settings.substring(0, 1) == '_') { return returnValue; } this.each(function() { var instance = instances[$.data(this, "jstree_instance_id")], methodValue = (instance && $.isFunction(instance[settings])) ? instance[settings].apply(instance, args) : instance; if(typeof methodValue !== "undefined" && (settings.indexOf("is_") === 0 || (methodValue !== true && methodValue !== false))) { returnValue = methodValue; return false; } }); } else { this.each(function() { // extend settings and allow for multiple hashes and $.data var instance_id = $.data(this, "jstree_instance_id"), a = [], b = settings ? $.extend({}, true, settings) : {}, c = $(this), s = false, t = []; a = a.concat(args); if(c.data("jstree")) { a.push(c.data("jstree")); } b = a.length ? $.extend.apply(null, [true, b].concat(a)) : b; // if an instance already exists, destroy it first if(typeof instance_id !== "undefined" && instances[instance_id]) { instances[instance_id].destroy(); } // push a new empty object to the instances array instance_id = parseInt(instances.push({}),10) - 1; // store the jstree instance id to the container element $.data(this, "jstree_instance_id", instance_id); // clean up all plugins b.plugins = $.isArray(b.plugins) ? b.plugins : $.jstree.defaults.plugins.slice(); b.plugins.unshift("core"); // only unique plugins b.plugins = b.plugins.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); // extend defaults with passed data s = $.extend(true, {}, $.jstree.defaults, b); s.plugins = b.plugins; $.each(plugins, function (i, val) { if($.inArray(i, s.plugins) === -1) { s[i] = null; delete s[i]; } else { t.push(i); } }); s.plugins = t; // push the new object to the instances array (at the same time set the default classes to the container) and init instances[instance_id] = new $.jstree._instance(instance_id, $(this).addClass("jstree jstree-" + instance_id), s); // init all activated plugins for this instance $.each(instances[instance_id]._get_settings().plugins, function (i, val) { instances[instance_id].data[val] = {}; }); $.each(instances[instance_id]._get_settings().plugins, function (i, val) { if(plugins[val]) { plugins[val].__init.apply(instances[instance_id]); } }); // initialize the instance setTimeout(function() { if(instances[instance_id]) { instances[instance_id].init(); } }, 0); }); } // return the jquery selection (or if it was a method call that returned a value - the returned value) return returnValue; }; // object to store exposed functions and objects $.jstree = { defaults : { plugins : [] }, _focused : function () { return instances[focused_instance] || null; }, _reference : function (needle) { // get by instance id if(instances[needle]) { return instances[needle]; } // get by DOM (if still no luck - return null var o = $(needle); if(!o.length && typeof needle === "string") { o = $("#" + needle); } if(!o.length) { return null; } return instances[o.closest(".jstree").data("jstree_instance_id")] || null; }, _instance : function (index, container, settings) { // for plugins to store data in this.data = { core : {} }; this.get_settings = function () { return $.extend(true, {}, settings); }; this._get_settings = function () { return settings; }; this.get_index = function () { return index; }; this.get_container = function () { return container; }; this.get_container_ul = function () { return container.children("ul:eq(0)"); }; this._set_settings = function (s) { settings = $.extend(true, {}, settings, s); }; }, _fn : { }, plugin : function (pname, pdata) { pdata = $.extend({}, { __init : $.noop, __destroy : $.noop, _fn : {}, defaults : false }, pdata); plugins[pname] = pdata; $.jstree.defaults[pname] = pdata.defaults; $.each(pdata._fn, function (i, val) { val.plugin = pname; val.old = $.jstree._fn[i]; $.jstree._fn[i] = function () { var rslt, func = val, args = Array.prototype.slice.call(arguments), evnt = new $.Event("before.jstree"), rlbk = false; if(this.data.core.locked === true && i !== "unlock" && i !== "is_locked") { return; } // Check if function belongs to the included plugins of this instance do { if(func && func.plugin && $.inArray(func.plugin, this._get_settings().plugins) !== -1) { break; } func = func.old; } while(func); if(!func) { return; } // context and function to trigger events, then finally call the function if(i.indexOf("_") === 0) { rslt = func.apply(this, args); } else { rslt = this.get_container().triggerHandler(evnt, { "func" : i, "inst" : this, "args" : args, "plugin" : func.plugin }); if(rslt === false) { return; } if(typeof rslt !== "undefined") { args = rslt; } rslt = func.apply( $.extend({}, this, { __callback : function (data) { this.get_container().triggerHandler( i + '.jstree', { "inst" : this, "args" : args, "rslt" : data, "rlbk" : rlbk }); }, __rollback : function () { rlbk = this.get_rollback(); return rlbk; }, __call_old : function (replace_arguments) { return func.old.apply(this, (replace_arguments ? Array.prototype.slice.call(arguments, 1) : args ) ); } }), args); } // return the result return rslt; }; $.jstree._fn[i].old = val.old; $.jstree._fn[i].plugin = pname; }); }, rollback : function (rb) { if(rb) { if(!$.isArray(rb)) { rb = [ rb ]; } $.each(rb, function (i, val) { instances[val.i].set_rollback(val.h, val.d); }); } } }; // set the prototype for all instances $.jstree._fn = $.jstree._instance.prototype = {}; // load the css when DOM is ready $(function() { // code is copied from jQuery ($.browser is deprecated + there is a bug in IE) var u = navigator.userAgent.toLowerCase(), v = (u.match( /.+?(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1], css_string = '' + '.jstree ul, .jstree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } ' + '.jstree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } ' + '.jstree-rtl li { margin-left:0; margin-right:18px; } ' + '.jstree > ul > li { margin-left:0px; } ' + '.jstree-rtl > ul > li { margin-right:0px; } ' + '.jstree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } ' + '.jstree a { display:inline-block; line-height:16px; height:16px; color:black; white-space:nowrap; text-decoration:none; padding:1px 2px; margin:0; } ' + '.jstree a:focus { outline: none; } ' + '.jstree a > ins { height:16px; width:16px; } ' + '.jstree a > .jstree-icon { margin-right:3px; } ' + '.jstree-rtl a > .jstree-icon { margin-left:3px; margin-right:0; } ' + 'li.jstree-open > ul { display:block; } ' + 'li.jstree-closed > ul { display:none; } '; // Correct IE 6 (does not support the > CSS selector) if(/msie/.test(u) && parseInt(v, 10) == 6) { is_ie6 = true; // fix image flicker and lack of caching try { document.execCommand("BackgroundImageCache", false, true); } catch (err) { } css_string += '' + '.jstree li { height:18px; margin-left:0; margin-right:0; } ' + '.jstree li li { margin-left:18px; } ' + '.jstree-rtl li li { margin-left:0px; margin-right:18px; } ' + 'li.jstree-open ul { display:block; } ' + 'li.jstree-closed ul { display:none !important; } ' + '.jstree li a { display:inline; border-width:0 !important; padding:0px 2px !important; } ' + '.jstree li a ins { height:16px; width:16px; margin-right:3px; } ' + '.jstree-rtl li a ins { margin-right:0px; margin-left:3px; } '; } // Correct IE 7 (shifts anchor nodes onhover) if(/msie/.test(u) && parseInt(v, 10) == 7) { is_ie7 = true; css_string += '.jstree li a { border-width:0 !important; padding:0px 2px !important; } '; } // correct ff2 lack of display:inline-block if(!/compatible/.test(u) && /mozilla/.test(u) && parseFloat(v, 10) < 1.9) { is_ff2 = true; css_string += '' + '.jstree ins { display:-moz-inline-box; } ' + '.jstree li { line-height:12px; } ' + // WHY?? '.jstree a { display:-moz-inline-box; } ' + '.jstree .jstree-no-icons .jstree-checkbox { display:-moz-inline-stack !important; } '; /* this shouldn't be here as it is theme specific */ } // the default stylesheet $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); // core functions (open, close, create, update, delete) $.jstree.plugin("core", { __init : function () { this.data.core.locked = false; this.data.core.to_open = this.get_settings().core.initially_open; this.data.core.to_load = this.get_settings().core.initially_load; }, defaults : { html_titles : false, animation : 500, initially_open : [], initially_load : [], open_parents : true, notify_plugins : true, rtl : false, load_open : false, strings : { loading : "Loading ...", new_node : "New node", multiple_selection : "Multiple selection" } }, _fn : { init : function () { this.set_focus(); if(this._get_settings().core.rtl) { this.get_container().addClass("jstree-rtl").css("direction", "rtl"); } this.get_container().html("<ul><li class='jstree-last jstree-leaf'><ins> </ins><a class='jstree-loading' href='#'><ins class='jstree-icon'> </ins>" + this._get_string("loading") + "</a></li></ul>"); this.data.core.li_height = this.get_container_ul().find("li.jstree-closed, li.jstree-leaf").eq(0).height() || 18; this.get_container() .delegate("li > ins", "click.jstree", $.proxy(function (event) { var trgt = $(event.target); // if(trgt.is("ins") && event.pageY - trgt.offset().top < this.data.core.li_height) { this.toggle_node(trgt); } this.toggle_node(trgt); }, this)) .bind("mousedown.jstree", $.proxy(function () { this.set_focus(); // This used to be setTimeout(set_focus,0) - why? }, this)) .bind("dblclick.jstree", function (event) { var sel; if(document.selection && document.selection.empty) { document.selection.empty(); } else { if(window.getSelection) { sel = window.getSelection(); try { sel.removeAllRanges(); sel.collapse(); } catch (err) { } } } }); if(this._get_settings().core.notify_plugins) { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var o = this._get_node(data.rslt.obj), t = this; if(o === -1) { o = this.get_container_ul(); } if(!o.length) { return; } o.find("li").each(function () { var th = $(this); if(th.data("jstree")) { $.each(th.data("jstree"), function (plugin, values) { if(t.data[plugin] && $.isFunction(t["_" + plugin + "_notify"])) { t["_" + plugin + "_notify"].call(t, th, values); } }); } }); }, this)); } if(this._get_settings().core.load_open) { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var o = this._get_node(data.rslt.obj), t = this; if(o === -1) { o = this.get_container_ul(); } if(!o.length) { return; } o.find("li.jstree-open:not(:has(ul))").each(function () { t.load_node(this, $.noop, $.noop); }); }, this)); } this.__callback(); this.load_node(-1, function () { this.loaded(); this.reload_nodes(); }); }, destroy : function () { var i, n = this.get_index(), s = this._get_settings(), _this = this; $.each(s.plugins, function (i, val) { try { plugins[val].__destroy.apply(_this); } catch(err) { } }); this.__callback(); // set focus to another instance if this one is focused if(this.is_focused()) { for(i in instances) { if(instances.hasOwnProperty(i) && i != n) { instances[i].set_focus(); break; } } } // if no other instance found if(n === focused_instance) { focused_instance = -1; } // remove all traces of jstree in the DOM (only the ones set using jstree*) and cleans all events this.get_container() .unbind(".jstree") .undelegate(".jstree") .removeData("jstree_instance_id") .find("[class^='jstree']") .andSelf() .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); $(document) .unbind(".jstree-" + n) .undelegate(".jstree-" + n); // remove the actual data instances[n] = null; delete instances[n]; }, _core_notify : function (n, data) { if(data.opened) { this.open_node(n, false, true); } }, lock : function () { this.data.core.locked = true; this.get_container().children("ul").addClass("jstree-locked").css("opacity","0.7"); this.__callback({}); }, unlock : function () { this.data.core.locked = false; this.get_container().children("ul").removeClass("jstree-locked").css("opacity","1"); this.__callback({}); }, is_locked : function () { return this.data.core.locked; }, save_opened : function () { var _this = this; this.data.core.to_open = []; this.get_container_ul().find("li.jstree-open").each(function () { if(this.id) { _this.data.core.to_open.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } }); this.__callback(_this.data.core.to_open); }, save_loaded : function () { }, reload_nodes : function (is_callback) { var _this = this, done = true, current = [], remaining = []; if(!is_callback) { this.data.core.reopen = false; this.data.core.refreshing = true; this.data.core.to_open = $.map($.makeArray(this.data.core.to_open), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); this.data.core.to_load = $.map($.makeArray(this.data.core.to_load), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); if(this.data.core.to_open.length) { this.data.core.to_load = this.data.core.to_load.concat(this.data.core.to_open); } } if(this.data.core.to_load.length) { $.each(this.data.core.to_load, function (i, val) { if(val == "#") { return true; } if($(val).length) { current.push(val); } else { remaining.push(val); } }); if(current.length) { this.data.core.to_load = remaining; $.each(current, function (i, val) { if(!_this._is_loaded(val)) { _this.load_node(val, function () { _this.reload_nodes(true); }, function () { _this.reload_nodes(true); }); done = false; } }); } } if(this.data.core.to_open.length) { $.each(this.data.core.to_open, function (i, val) { _this.open_node(val, false, true); }); } if(done) { // TODO: find a more elegant approach to syncronizing returning requests if(this.data.core.reopen) { clearTimeout(this.data.core.reopen); } this.data.core.reopen = setTimeout(function () { _this.__callback({}, _this); }, 50); this.data.core.refreshing = false; this.reopen(); } }, reopen : function () { var _this = this; if(this.data.core.to_open.length) { $.each(this.data.core.to_open, function (i, val) { _this.open_node(val, false, true); }); } this.__callback({}); }, refresh : function (obj) { var _this = this; this.save_opened(); if(!obj) { obj = -1; } obj = this._get_node(obj); if(!obj) { obj = -1; } if(obj !== -1) { obj.children("UL").remove(); } else { this.get_container_ul().empty(); } this.load_node(obj, function () { _this.__callback({ "obj" : obj}); _this.reload_nodes(); }); }, // Dummy function to fire after the first load (so that there is a jstree.loaded event) loaded : function () { this.__callback(); }, // deal with focus set_focus : function () { if(this.is_focused()) { return; } var f = $.jstree._focused(); if(f) { f.unset_focus(); } this.get_container().addClass("jstree-focused"); focused_instance = this.get_index(); this.__callback(); }, is_focused : function () { return focused_instance == this.get_index(); }, unset_focus : function () { if(this.is_focused()) { this.get_container().removeClass("jstree-focused"); focused_instance = -1; } this.__callback(); }, // traverse _get_node : function (obj) { var $obj = $(obj, this.get_container()); if($obj.is(".jstree") || obj == -1) { return -1; } $obj = $obj.closest("li", this.get_container()); return $obj.length ? $obj : false; }, _get_next : function (obj, strict) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().find("> ul > li:first-child"); } if(!obj.length) { return false; } if(strict) { return (obj.nextAll("li").size() > 0) ? obj.nextAll("li:eq(0)") : false; } if(obj.hasClass("jstree-open")) { return obj.find("li:eq(0)"); } else if(obj.nextAll("li").size() > 0) { return obj.nextAll("li:eq(0)"); } else { return obj.parentsUntil(".jstree","li").next("li").eq(0); } }, _get_prev : function (obj, strict) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().find("> ul > li:last-child"); } if(!obj.length) { return false; } if(strict) { return (obj.prevAll("li").length > 0) ? obj.prevAll("li:eq(0)") : false; } if(obj.prev("li").length) { obj = obj.prev("li").eq(0); while(obj.hasClass("jstree-open")) { obj = obj.children("ul:eq(0)").children("li:last"); } return obj; } else { var o = obj.parentsUntil(".jstree","li:eq(0)"); return o.length ? o : false; } }, _get_parent : function (obj) { obj = this._get_node(obj); if(obj == -1 || !obj.length) { return false; } var o = obj.parentsUntil(".jstree", "li:eq(0)"); return o.length ? o : -1; }, _get_children : function (obj) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().children("ul:eq(0)").children("li"); } if(!obj.length) { return false; } return obj.children("ul:eq(0)").children("li"); }, get_path : function (obj, id_mode) { var p = [], _this = this; obj = this._get_node(obj); if(obj === -1 || !obj || !obj.length) { return false; } obj.parentsUntil(".jstree", "li").each(function () { p.push( id_mode ? this.id : _this.get_text(this) ); }); p.reverse(); p.push( id_mode ? obj.attr("id") : this.get_text(obj) ); return p; }, // string functions _get_string : function (key) { return this._get_settings().core.strings[key] || key; }, is_open : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-open"); }, is_closed : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-closed"); }, is_leaf : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-leaf"); }, correct_state : function (obj) { obj = this._get_node(obj); if(!obj || obj === -1) { return false; } obj.removeClass("jstree-closed jstree-open").addClass("jstree-leaf").children("ul").remove(); this.__callback({ "obj" : obj }); }, // open/close open_node : function (obj, callback, skip_animation) { obj = this._get_node(obj); if(!obj.length) { return false; } if(!obj.hasClass("jstree-closed")) { if(callback) { callback.call(); } return false; } var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, t = this; if(!this._is_loaded(obj)) { obj.children("a").addClass("jstree-loading"); this.load_node(obj, function () { t.open_node(obj, callback, skip_animation); }, callback); } else { if(this._get_settings().core.open_parents) { obj.parentsUntil(".jstree",".jstree-closed").each(function () { t.open_node(this, false, true); }); } if(s) { obj.children("ul").css("display","none"); } obj.removeClass("jstree-closed").addClass("jstree-open").children("a").removeClass("jstree-loading"); if(s) { obj.children("ul").stop(true, true).slideDown(s, function () { this.style.display = ""; t.after_open(obj); }); } else { t.after_open(obj); } this.__callback({ "obj" : obj }); if(callback) { callback.call(); } } }, after_open : function (obj) { this.__callback({ "obj" : obj }); }, close_node : function (obj, skip_animation) { obj = this._get_node(obj); var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, t = this; if(!obj.length || !obj.hasClass("jstree-open")) { return false; } if(s) { obj.children("ul").attr("style","display:block !important"); } obj.removeClass("jstree-open").addClass("jstree-closed"); if(s) { obj.children("ul").stop(true, true).slideUp(s, function () { this.style.display = ""; t.after_close(obj); }); } else { t.after_close(obj); } this.__callback({ "obj" : obj }); }, after_close : function (obj) { this.__callback({ "obj" : obj }); }, toggle_node : function (obj) { obj = this._get_node(obj); if(obj.hasClass("jstree-closed")) { return this.open_node(obj); } if(obj.hasClass("jstree-open")) { return this.close_node(obj); } }, open_all : function (obj, do_animation, original_obj) { obj = obj ? this._get_node(obj) : -1; if(!obj || obj === -1) { obj = this.get_container_ul(); } if(original_obj) { obj = obj.find("li.jstree-closed"); } else { original_obj = obj; if(obj.is(".jstree-closed")) { obj = obj.find("li.jstree-closed").andSelf(); } else { obj = obj.find("li.jstree-closed"); } } var _this = this; obj.each(function () { var __this = this; if(!_this._is_loaded(this)) { _this.open_node(this, function() { _this.open_all(__this, do_animation, original_obj); }, !do_animation); } else { _this.open_node(this, false, !do_animation); } }); // so that callback is fired AFTER all nodes are open if(original_obj.find('li.jstree-closed').length === 0) { this.__callback({ "obj" : original_obj }); } }, close_all : function (obj, do_animation) { var _this = this; obj = obj ? this._get_node(obj) : this.get_container(); if(!obj || obj === -1) { obj = this.get_container_ul(); } obj.find("li.jstree-open").andSelf().each(function () { _this.close_node(this, !do_animation); }); this.__callback({ "obj" : obj }); }, clean_node : function (obj) { obj = obj && obj != -1 ? $(obj) : this.get_container_ul(); obj = obj.is("li") ? obj.find("li").andSelf() : obj.find("li"); obj.removeClass("jstree-last") .filter("li:last-child").addClass("jstree-last").end() .filter(":has(li)") .not(".jstree-open").removeClass("jstree-leaf").addClass("jstree-closed"); obj.not(".jstree-open, .jstree-closed").addClass("jstree-leaf").children("ul").remove(); this.__callback({ "obj" : obj }); }, // rollback get_rollback : function () { this.__callback(); return { i : this.get_index(), h : this.get_container().children("ul").clone(true), d : this.data }; }, set_rollback : function (html, data) { this.get_container().empty().append(html); this.data = data; this.__callback(); }, // Dummy functions to be overwritten by any datastore plugin included load_node : function (obj, s_call, e_call) { this.__callback({ "obj" : obj }); }, _is_loaded : function (obj) { return true; }, // Basic operations: create create_node : function (obj, position, js, callback, is_loaded) { obj = this._get_node(obj); position = typeof position === "undefined" ? "last" : position; var d = $("<li />"), s = this._get_settings().core, tmp; if(obj !== -1 && !obj.length) { return false; } if(!is_loaded && !this._is_loaded(obj)) { this.load_node(obj, function () { this.create_node(obj, position, js, callback, true); }); return false; } this.__rollback(); if(typeof js === "string") { js = { "data" : js }; } if(!js) { js = {}; } if(js.attr) { d.attr(js.attr); } if(js.metadata) { d.data(js.metadata); } if(js.state) { d.addClass("jstree-" + js.state); } if(!js.data) { js.data = this._get_string("new_node"); } if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } $.each(js.data, function (i, m) { tmp = $("<a />"); if($.isFunction(m)) { m = m.call(this, js); } if(typeof m == "string") { tmp.attr('href','#')[ s.html_titles ? "html" : "text" ](m); } else { if(!m.attr) { m.attr = {}; } if(!m.attr.href) { m.attr.href = '#'; } tmp.attr(m.attr)[ s.html_titles ? "html" : "text" ](m.title); if(m.language) { tmp.addClass(m.language); } } tmp.prepend("<ins class='jstree-icon'> </ins>"); if(!m.icon && js.icon) { m.icon = js.icon; } if(m.icon) { if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } } d.append(tmp); }); d.prepend("<ins class='jstree-icon'> </ins>"); if(obj === -1) { obj = this.get_container(); if(position === "before") { position = "first"; } if(position === "after") { position = "last"; } } switch(position) { case "before": obj.before(d); tmp = this._get_parent(obj); break; case "after" : obj.after(d); tmp = this._get_parent(obj); break; case "inside": case "first" : if(!obj.children("ul").length) { obj.append("<ul />"); } obj.children("ul").prepend(d); tmp = obj; break; case "last": if(!obj.children("ul").length) { obj.append("<ul />"); } obj.children("ul").append(d); tmp = obj; break; default: if(!obj.children("ul").length) { obj.append("<ul />"); } if(!position) { position = 0; } tmp = obj.children("ul").children("li").eq(position); if(tmp.length) { tmp.before(d); } else { obj.children("ul").append(d); } tmp = obj; break; } if(tmp === -1 || tmp.get(0) === this.get_container().get(0)) { tmp = -1; } this.clean_node(tmp); this.__callback({ "obj" : d, "parent" : tmp }); if(callback) { callback.call(this, d); } return d; }, // Basic operations: rename (deal with text) get_text : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } var s = this._get_settings().core.html_titles; obj = obj.children("a:eq(0)"); if(s) { obj = obj.clone(); obj.children("INS").remove(); return obj.html(); } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; return obj.nodeValue; } }, set_text : function (obj, val) { obj = this._get_node(obj); if(!obj.length) { return false; } obj = obj.children("a:eq(0)"); if(this._get_settings().core.html_titles) { var tmp = obj.children("INS").clone(); obj.html(val).prepend(tmp); this.__callback({ "obj" : obj, "name" : val }); return true; } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; this.__callback({ "obj" : obj, "name" : val }); return (obj.nodeValue = val); } }, rename_node : function (obj, val) { obj = this._get_node(obj); this.__rollback(); if(obj && obj.length && this.set_text.apply(this, Array.prototype.slice.call(arguments))) { this.__callback({ "obj" : obj, "name" : val }); } }, // Basic operations: deleting nodes delete_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } this.__rollback(); var p = this._get_parent(obj), prev = $([]), t = this; obj.each(function () { prev = prev.add(t._get_prev(this)); }); obj = obj.detach(); if(p !== -1 && p.find("> ul > li").length === 0) { p.removeClass("jstree-open jstree-closed").addClass("jstree-leaf"); } this.clean_node(p); this.__callback({ "obj" : obj, "prev" : prev, "parent" : p }); return obj; }, prepare_move : function (o, r, pos, cb, is_cb) { var p = {}; p.ot = $.jstree._reference(o) || this; p.o = p.ot._get_node(o); p.r = r === - 1 ? -1 : this._get_node(r); p.p = (typeof pos === "undefined" || pos === false) ? "last" : pos; // TODO: move to a setting if(!is_cb && prepared_move.o && prepared_move.o[0] === p.o[0] && prepared_move.r[0] === p.r[0] && prepared_move.p === p.p) { this.__callback(prepared_move); if(cb) { cb.call(this, prepared_move); } return; } p.ot = $.jstree._reference(p.o) || this; p.rt = $.jstree._reference(p.r) || this; // r === -1 ? p.ot : $.jstree._reference(p.r) || this if(p.r === -1 || !p.r) { p.cr = -1; switch(p.p) { case "first": case "before": case "inside": p.cp = 0; break; case "after": case "last": p.cp = p.rt.get_container().find(" > ul > li").length; break; default: p.cp = p.p; break; } } else { if(!/^(before|after)$/.test(p.p) && !this._is_loaded(p.r)) { return this.load_node(p.r, function () { this.prepare_move(o, r, pos, cb, true); }); } switch(p.p) { case "before": p.cp = p.r.index(); p.cr = p.rt._get_parent(p.r); break; case "after": p.cp = p.r.index() + 1; p.cr = p.rt._get_parent(p.r); break; case "inside": case "first": p.cp = 0; p.cr = p.r; break; case "last": p.cp = p.r.find(" > ul > li").length; p.cr = p.r; break; default: p.cp = p.p; p.cr = p.r; break; } } p.np = p.cr == -1 ? p.rt.get_container() : p.cr; p.op = p.ot._get_parent(p.o); p.cop = p.o.index(); if(p.op === -1) { p.op = p.ot ? p.ot.get_container() : this.get_container(); } if(!/^(before|after)$/.test(p.p) && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp++; } //if(p.p === "before" && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp--; } p.or = p.np.find(" > ul > li:nth-child(" + (p.cp + 1) + ")"); prepared_move = p; this.__callback(prepared_move); if(cb) { cb.call(this, prepared_move); } }, check_move : function () { var obj = prepared_move, ret = true, r = obj.r === -1 ? this.get_container() : obj.r; if(!obj || !obj.o || obj.or[0] === obj.o[0]) { return false; } if(obj.op && obj.np && obj.op[0] === obj.np[0] && obj.cp - 1 === obj.o.index()) { return false; } obj.o.each(function () { if(r.parentsUntil(".jstree", "li").andSelf().index(this) !== -1) { ret = false; return false; } }); return ret; }, move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) { if(!is_prepared) { return this.prepare_move(obj, ref, position, function (p) { this.move_node(p, false, false, is_copy, true, skip_check); }); } if(is_copy) { prepared_move.cy = true; } if(!skip_check && !this.check_move()) { return false; } this.__rollback(); var o = false; if(is_copy) { o = obj.o.clone(true); o.find("*[id]").andSelf().each(function () { if(this.id) { this.id = "copy_" + this.id; } }); } else { o = obj.o; } if(obj.or.length) { obj.or.before(o); } else { if(!obj.np.children("ul").length) { $("<ul />").appendTo(obj.np); } obj.np.children("ul:eq(0)").append(o); } try { obj.ot.clean_node(obj.op); obj.rt.clean_node(obj.np); if(!obj.op.find("> ul > li").length) { obj.op.removeClass("jstree-open jstree-closed").addClass("jstree-leaf").children("ul").remove(); } } catch (e) { } if(is_copy) { prepared_move.cy = true; prepared_move.oc = o; } this.__callback(prepared_move); return prepared_move; }, _get_move : function () { return prepared_move; } } }); })(jQuery); //*/ /* * jsTree ui plugin * This plugins handles selecting/deselecting/hovering/dehovering nodes */ (function ($) { var scrollbar_width, e1, e2; $(function() { if (/msie/.test(navigator.userAgent.toLowerCase())) { e1 = $('<textarea cols="10" rows="2"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body'); e2 = $('<textarea cols="10" rows="2" style="overflow: hidden;"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body'); scrollbar_width = e1.width() - e2.width(); e1.add(e2).remove(); } else { e1 = $('<div />').css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: 0 }) .prependTo('body').append('<div />').find('div').css({ width: '100%', height: 200 }); scrollbar_width = 100 - e1.width(); e1.parent().remove(); } }); $.jstree.plugin("ui", { __init : function () { this.data.ui.selected = $(); this.data.ui.last_selected = false; this.data.ui.hovered = null; this.data.ui.to_select = this.get_settings().ui.initially_select; this.get_container() .delegate("a", "click.jstree", $.proxy(function (event) { event.preventDefault(); event.currentTarget.blur(); if(!$(event.currentTarget).hasClass("jstree-loading")) { this.select_node(event.currentTarget, true, event); } }, this)) .delegate("a", "mouseenter.jstree", $.proxy(function (event) { if(!$(event.currentTarget).hasClass("jstree-loading")) { this.hover_node(event.target); } }, this)) .delegate("a", "mouseleave.jstree", $.proxy(function (event) { if(!$(event.currentTarget).hasClass("jstree-loading")) { this.dehover_node(event.target); } }, this)) .bind("reopen.jstree", $.proxy(function () { this.reselect(); }, this)) .bind("get_rollback.jstree", $.proxy(function () { this.dehover_node(); this.save_selected(); }, this)) .bind("set_rollback.jstree", $.proxy(function () { this.reselect(); }, this)) .bind("close_node.jstree", $.proxy(function (event, data) { var s = this._get_settings().ui, obj = this._get_node(data.rslt.obj), clk = (obj && obj.length) ? obj.children("ul").find("a.jstree-clicked") : $(), _this = this; if(s.selected_parent_close === false || !clk.length) { return; } clk.each(function () { _this.deselect_node(this); if(s.selected_parent_close === "select_parent") { _this.select_node(obj); } }); }, this)) .bind("delete_node.jstree", $.proxy(function (event, data) { var s = this._get_settings().ui.select_prev_on_delete, obj = this._get_node(data.rslt.obj), clk = (obj && obj.length) ? obj.find("a.jstree-clicked") : [], _this = this; clk.each(function () { _this.deselect_node(this); }); if(s && clk.length) { data.rslt.prev.each(function () { if(this.parentNode) { _this.select_node(this); return false; /* if return false is removed all prev nodes will be selected */} }); } }, this)) .bind("move_node.jstree", $.proxy(function (event, data) { if(data.rslt.cy) { data.rslt.oc.find("a.jstree-clicked").removeClass("jstree-clicked"); } }, this)); }, defaults : { select_limit : -1, // 0, 1, 2 ... or -1 for unlimited select_multiple_modifier : "ctrl", // on, or ctrl, shift, alt select_range_modifier : "shift", selected_parent_close : "select_parent", // false, "deselect", "select_parent" selected_parent_open : true, select_prev_on_delete : true, disable_selecting_children : false, initially_select : [] }, _fn : { _get_node : function (obj, allow_multiple) { if(typeof obj === "undefined" || obj === null) { return allow_multiple ? this.data.ui.selected : this.data.ui.last_selected; } var $obj = $(obj, this.get_container()); if($obj.is(".jstree") || obj == -1) { return -1; } $obj = $obj.closest("li", this.get_container()); return $obj.length ? $obj : false; }, _ui_notify : function (n, data) { if(data.selected) { this.select_node(n, false); } }, save_selected : function () { var _this = this; this.data.ui.to_select = []; this.data.ui.selected.each(function () { if(this.id) { _this.data.ui.to_select.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } }); this.__callback(this.data.ui.to_select); }, reselect : function () { var _this = this, s = this.data.ui.to_select; s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); // this.deselect_all(); WHY deselect, breaks plugin state notifier? $.each(s, function (i, val) { if(val && val !== "#") { _this.select_node(val); } }); this.data.ui.selected = this.data.ui.selected.filter(function () { return this.parentNode; }); this.__callback(); }, refresh : function (obj) { this.save_selected(); return this.__call_old(); }, hover_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } //if(this.data.ui.hovered && obj.get(0) === this.data.ui.hovered.get(0)) { return; } if(!obj.hasClass("jstree-hovered")) { this.dehover_node(); } this.data.ui.hovered = obj.children("a").addClass("jstree-hovered").parent(); // brackets: jstree uses default scroll bar widths, and there's no clean way to override the code from brackets, which causes continuous scrolling, so just disable for now //this._fix_scroll(obj); this.__callback({ "obj" : obj }); }, dehover_node : function () { var obj = this.data.ui.hovered, p; if(!obj || !obj.length) { return false; } p = obj.children("a").removeClass("jstree-hovered").parent(); if(this.data.ui.hovered[0] === p[0]) { this.data.ui.hovered = null; } this.__callback({ "obj" : obj }); }, select_node : function (obj, check, e) { obj = this._get_node(obj); if(obj == -1 || !obj || !obj.length) { return false; } var s = this._get_settings().ui, is_multiple = (s.select_multiple_modifier == "on" || (s.select_multiple_modifier !== false && e && e[s.select_multiple_modifier + "Key"])), is_range = (s.select_range_modifier !== false && e && e[s.select_range_modifier + "Key"] && this.data.ui.last_selected && this.data.ui.last_selected[0] !== obj[0] && this.data.ui.last_selected.parent()[0] === obj.parent()[0]), is_selected = this.is_selected(obj), proceed = true, t = this; if(check) { if(s.disable_selecting_children && is_multiple && ( (obj.parentsUntil(".jstree","li").children("a.jstree-clicked").length) || (obj.children("ul").find("a.jstree-clicked:eq(0)").length) ) ) { return false; } proceed = false; switch(!0) { case (is_range): this.data.ui.last_selected.addClass("jstree-last-selected"); obj = obj[ obj.index() < this.data.ui.last_selected.index() ? "nextUntil" : "prevUntil" ](".jstree-last-selected").andSelf(); if(s.select_limit == -1 || obj.length < s.select_limit) { this.data.ui.last_selected.removeClass("jstree-last-selected"); this.data.ui.selected.each(function () { if(this !== t.data.ui.last_selected[0]) { t.deselect_node(this); } }); is_selected = false; proceed = true; } else { proceed = false; } break; case (is_selected && !is_multiple): this.deselect_all(); is_selected = false; proceed = true; break; case (!is_selected && !is_multiple): if(s.select_limit == -1 || s.select_limit > 0) { this.deselect_all(); proceed = true; } break; case (is_selected && is_multiple): this.deselect_node(obj); break; case (!is_selected && is_multiple): if(s.select_limit == -1 || this.data.ui.selected.length + 1 <= s.select_limit) { proceed = true; } break; } } if(proceed && !is_selected) { if(!is_range) { this.data.ui.last_selected = obj; } obj.children("a").addClass("jstree-clicked"); if(s.selected_parent_open) { obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); } this.data.ui.selected = this.data.ui.selected.add(obj); this._fix_scroll(obj.eq(0)); this.__callback({ "obj" : obj, "e" : e }); } }, _fix_scroll : function (obj) { var c = this.get_container()[0], t; if(c.scrollHeight > c.offsetHeight) { obj = this._get_node(obj); if(!obj || obj === -1 || !obj.length || !obj.is(":visible")) { return; } t = obj.offset().top - this.get_container().offset().top; if(t < 0) { c.scrollTop = c.scrollTop + t - 1; } if(t + this.data.core.li_height + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0) > c.offsetHeight) { c.scrollTop = c.scrollTop + (t - c.offsetHeight + this.data.core.li_height + 1 + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0)); } } }, deselect_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } if(this.is_selected(obj)) { obj.children("a").removeClass("jstree-clicked"); this.data.ui.selected = this.data.ui.selected.not(obj); if(this.data.ui.last_selected.get(0) === obj.get(0)) { this.data.ui.last_selected = this.data.ui.selected.eq(0); } this.__callback({ "obj" : obj }); } }, toggle_select : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } if(this.is_selected(obj)) { this.deselect_node(obj); } else { this.select_node(obj); } }, is_selected : function (obj) { return this.data.ui.selected.index(this._get_node(obj)) >= 0; }, get_selected : function (context) { return context ? $(context).find("a.jstree-clicked").parent() : this.data.ui.selected; }, deselect_all : function (context) { var ret = context ? $(context).find("a.jstree-clicked").parent() : this.get_container().find("a.jstree-clicked").parent(); ret.children("a.jstree-clicked").removeClass("jstree-clicked"); this.data.ui.selected = $([]); this.data.ui.last_selected = false; this.__callback({ "obj" : ret }); } } }); // include the selection plugin by default $.jstree.defaults.plugins.push("ui"); })(jQuery); //*/ /* * jsTree CRRM plugin * Handles creating/renaming/removing/moving nodes by user interaction. */ (function ($) { $.jstree.plugin("crrm", { __init : function () { this.get_container() .bind("move_node.jstree", $.proxy(function (e, data) { if(this._get_settings().crrm.move.open_onmove) { var t = this; data.rslt.np.parentsUntil(".jstree").andSelf().filter(".jstree-closed").each(function () { t.open_node(this, false, true); }); } }, this)); }, defaults : { input_width_limit : 200, move : { always_copy : false, // false, true or "multitree" open_onmove : true, default_position : "last", check_move : function (m) { return true; } } }, _fn : { _show_input : function (obj, callback) { obj = this._get_node(obj); var rtl = this._get_settings().core.rtl, w = this._get_settings().crrm.input_width_limit, w1 = obj.children("ins").width(), w2 = obj.find("> a:visible > ins").width() * obj.find("> a:visible > ins").length, t = this.get_text(obj), h1 = $("<div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"), h2 = obj.css("position","relative").append( $("<input />", { "value" : t, "class" : "jstree-rename-input", // "size" : t.length, "css" : { "padding" : "0", "border" : "1px solid silver", "position" : "absolute", "left" : (rtl ? "auto" : (w1 + w2 + 4) + "px"), "right" : (rtl ? (w1 + w2 + 4) + "px" : "auto"), "top" : "0px", "height" : (this.data.core.li_height - 2) + "px", "lineHeight" : (this.data.core.li_height - 2) + "px", "width" : "150px" // will be set a bit further down }, "blur" : $.proxy(function () { var i = obj.children(".jstree-rename-input"), v = i.val(); if(v === "") { v = t; } h1.remove(); i.remove(); // rollback purposes this.set_text(obj,t); // rollback purposes this.rename_node(obj, v); callback.call(this, obj, v, t); obj.css("position",""); }, this), "keyup" : function (event) { var key = event.keyCode || event.which; if(key == 27) { this.value = t; this.blur(); return; } else if(key == 13) { this.blur(); return; } else { h2.width(Math.min(h1.text("pW" + this.value).width(),w)); } }, "keypress" : function(event) { var key = event.keyCode || event.which; if(key == 13) { return false; } } }) ).children(".jstree-rename-input"); this.set_text(obj, ""); h1.css({ fontFamily : h2.css('fontFamily') || '', fontSize : h2.css('fontSize') || '', fontWeight : h2.css('fontWeight') || '', fontStyle : h2.css('fontStyle') || '', fontStretch : h2.css('fontStretch') || '', fontVariant : h2.css('fontVariant') || '', letterSpacing : h2.css('letterSpacing') || '', wordSpacing : h2.css('wordSpacing') || '' }); h2.width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); }, rename : function (obj) { obj = this._get_node(obj); this.__rollback(); var f = this.__callback; this._show_input(obj, function (obj, new_name, old_name) { f.call(this, { "obj" : obj, "new_name" : new_name, "old_name" : old_name }); }); }, create : function (obj, position, js, callback, skip_rename) { var t, _this = this; obj = this._get_node(obj); if(!obj) { obj = -1; } this.__rollback(); t = this.create_node(obj, position, js, function (t) { var p = this._get_parent(t), pos = $(t).index(); if(callback) { callback.call(this, t); } if(p.length && p.hasClass("jstree-closed")) { this.open_node(p, false, true); } if(!skip_rename) { this._show_input(t, function (obj, new_name, old_name) { _this.__callback({ "obj" : obj, "name" : new_name, "parent" : p, "position" : pos }); }); } else { _this.__callback({ "obj" : t, "name" : this.get_text(t), "parent" : p, "position" : pos }); } }); return t; }, remove : function (obj) { obj = this._get_node(obj, true); var p = this._get_parent(obj), prev = this._get_prev(obj); this.__rollback(); obj = this.delete_node(obj); if(obj !== false) { this.__callback({ "obj" : obj, "prev" : prev, "parent" : p }); } }, check_move : function () { if(!this.__call_old()) { return false; } var s = this._get_settings().crrm.move; if(!s.check_move.call(this, this._get_move())) { return false; } return true; }, move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) { var s = this._get_settings().crrm.move; if(!is_prepared) { if(typeof position === "undefined") { position = s.default_position; } if(position === "inside" && !s.default_position.match(/^(before|after)$/)) { position = s.default_position; } return this.__call_old(true, obj, ref, position, is_copy, false, skip_check); } // if the move is already prepared if(s.always_copy === true || (s.always_copy === "multitree" && obj.rt.get_index() !== obj.ot.get_index() )) { is_copy = true; } this.__call_old(true, obj, ref, position, is_copy, true, skip_check); }, cut : function (obj) { obj = this._get_node(obj, true); if(!obj || !obj.length) { return false; } this.data.crrm.cp_nodes = false; this.data.crrm.ct_nodes = obj; this.__callback({ "obj" : obj }); }, copy : function (obj) { obj = this._get_node(obj, true); if(!obj || !obj.length) { return false; } this.data.crrm.ct_nodes = false; this.data.crrm.cp_nodes = obj; this.__callback({ "obj" : obj }); }, paste : function (obj) { obj = this._get_node(obj); if(!obj || !obj.length) { return false; } var nodes = this.data.crrm.ct_nodes ? this.data.crrm.ct_nodes : this.data.crrm.cp_nodes; if(!this.data.crrm.ct_nodes && !this.data.crrm.cp_nodes) { return false; } if(this.data.crrm.ct_nodes) { this.move_node(this.data.crrm.ct_nodes, obj); this.data.crrm.ct_nodes = false; } if(this.data.crrm.cp_nodes) { this.move_node(this.data.crrm.cp_nodes, obj, false, true); } this.__callback({ "obj" : obj, "nodes" : nodes }); } } }); // include the crr plugin by default // $.jstree.defaults.plugins.push("crrm"); })(jQuery); //*/ /* * jsTree themes plugin * Handles loading and setting themes, as well as detecting path to themes, etc. */ (function ($) { var themes_loaded = []; // this variable stores the path to the themes folder - if left as false - it will be autodetected $.jstree._themes = false; $.jstree.plugin("themes", { __init : function () { this.get_container() .bind("init.jstree", $.proxy(function () { var s = this._get_settings().themes; this.data.themes.dots = s.dots; this.data.themes.icons = s.icons; this.set_theme(s.theme, s.url); }, this)) .bind("loaded.jstree", $.proxy(function () { // bound here too, as simple HTML tree's won't honor dots & icons otherwise if(!this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } if(!this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } }, this)); }, defaults : { theme : "default", url : false, dots : true, icons : true }, _fn : { set_theme : function (theme_name, theme_url) { if(!theme_name) { return false; } if(!theme_url) { theme_url = $.jstree._themes + theme_name + '/style.css'; } if($.inArray(theme_url, themes_loaded) == -1) { $.vakata.css.add_sheet({ "url" : theme_url }); themes_loaded.push(theme_url); } if(this.data.themes.theme != theme_name) { this.get_container().removeClass('jstree-' + this.data.themes.theme); this.data.themes.theme = theme_name; } this.get_container().addClass('jstree-' + theme_name); if(!this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } if(!this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } this.__callback(); }, get_theme : function () { return this.data.themes.theme; }, show_dots : function () { this.data.themes.dots = true; this.get_container().children("ul").removeClass("jstree-no-dots"); }, hide_dots : function () { this.data.themes.dots = false; this.get_container().children("ul").addClass("jstree-no-dots"); }, toggle_dots : function () { if(this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } }, show_icons : function () { this.data.themes.icons = true; this.get_container().children("ul").removeClass("jstree-no-icons"); }, hide_icons : function () { this.data.themes.icons = false; this.get_container().children("ul").addClass("jstree-no-icons"); }, toggle_icons: function () { if(this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } } } }); // autodetect themes path $(function () { if($.jstree._themes === false) { $("script").each(function () { if(this.src.toString().match(/jquery\.jstree[^\/]*?\.js(\?.*)?$/)) { $.jstree._themes = this.src.toString().replace(/jquery\.jstree[^\/]*?\.js(\?.*)?$/, "") + 'themes/'; return false; } }); } if($.jstree._themes === false) { $.jstree._themes = "themes/"; } }); // include the themes plugin by default $.jstree.defaults.plugins.push("themes"); })(jQuery); //*/ /* * jsTree hotkeys plugin * Enables keyboard navigation for all tree instances * Depends on the jstree ui & jquery hotkeys plugins */ (function ($) { var bound = []; function exec(i, event) { var f = $.jstree._focused(), tmp; if(f && f.data && f.data.hotkeys && f.data.hotkeys.enabled) { tmp = f._get_settings().hotkeys[i]; if(tmp) { return tmp.call(f, event); } } } $.jstree.plugin("hotkeys", { __init : function () { if(typeof $.hotkeys === "undefined") { throw "jsTree hotkeys: jQuery hotkeys plugin not included."; } if(!this.data.ui) { throw "jsTree hotkeys: jsTree UI plugin not included."; } $.each(this._get_settings().hotkeys, function (i, v) { if(v !== false && $.inArray(i, bound) == -1) { $(document).bind("keydown", i, function (event) { return exec(i, event); }); bound.push(i); } }); this.get_container() .bind("lock.jstree", $.proxy(function () { if(this.data.hotkeys.enabled) { this.data.hotkeys.enabled = false; this.data.hotkeys.revert = true; } }, this)) .bind("unlock.jstree", $.proxy(function () { if(this.data.hotkeys.revert) { this.data.hotkeys.enabled = true; } }, this)); this.enable_hotkeys(); }, defaults : { "up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "ctrl+up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "shift+up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "ctrl+down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "shift+down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "ctrl+left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "shift+left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "ctrl+right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "shift+right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "space" : function () { if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").click(); } return false; }, "ctrl+space" : function (event) { event.type = "click"; if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); } return false; }, "shift+space" : function (event) { event.type = "click"; if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); } return false; }, "f2" : function () { this.rename(this.data.ui.hovered || this.data.ui.last_selected); }, "del" : function () { this.remove(this.data.ui.hovered || this._get_node(null)); } }, _fn : { enable_hotkeys : function () { this.data.hotkeys.enabled = true; }, disable_hotkeys : function () { this.data.hotkeys.enabled = false; } } }); })(jQuery); //*/ /* * jsTree JSON plugin * The JSON data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions. */ (function ($) { $.jstree.plugin("json_data", { __init : function() { var s = this._get_settings().json_data; if(s.progressive_unload) { this.get_container().bind("after_close.jstree", function (e, data) { data.rslt.obj.children("ul").remove(); }); } }, defaults : { // `data` can be a function: // * accepts two arguments - node being loaded and a callback to pass the result to // * will be executed in the current tree's scope & ajax won't be supported data : false, ajax : false, correct_state : true, progressive_render : false, progressive_unload : false }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_json(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { var s = this._get_settings().json_data; obj = this._get_node(obj); return obj == -1 || !obj || (!s.ajax && !s.progressive_render && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").length > 0; }, refresh : function (obj) { obj = this._get_node(obj); var s = this._get_settings().json_data; if(obj && obj !== -1 && s.progressive_unload && ($.isFunction(s.data) || !!s.ajax)) { obj.removeData("jstree_children"); } return this.__call_old(); }, load_node_json : function (obj, s_call, e_call) { var s = this.get_settings().json_data, d, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1 && (s.progressive_render || s.progressive_unload) && !obj.is(".jstree-open, .jstree-leaf") && obj.children("ul").children("li").length === 0 && obj.data("jstree_children")) { d = this._parse_json(obj.data("jstree_children"), obj); if(d) { obj.append(d); if(!s.progressive_unload) { obj.removeData("jstree_children"); } } this.clean_node(obj); if(s_call) { s_call.call(this); } return; } if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied."; // function option added here for easier model integration (also supporting async - see callback) case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { d = this._parse_json(d, obj); if(!d) { if(obj === -1 || !obj) { if(s.correct_state) { this.get_container().children("ul").empty(); } } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); } } if(e_call) { e_call.call(this); } } else { if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } }, this)); break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { d = this._parse_json(s.data, obj); if(d) { this.get_container().children("ul").empty().append(d.children()); this.clean_node(); } else { if(s.correct_state) { this.get_container().children("ul").empty(); } } } if(s_call) { s_call.call(this); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): error_func = function (x, t, e) { var ef = this.get_settings().json_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj != -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { var sf = this.get_settings().json_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "") || (!$.isArray(d) && !$.isPlainObject(d))) { return error_func.call(this, x, t, ""); } d = this._parse_json(d, obj); if(d) { if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj === -1 || !obj) { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } } }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "json"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } }, _parse_json : function (js, obj, is_callback) { var d = false, p = this._get_settings(), s = p.json_data, t = p.core.html_titles, tmp, i, j, ul1, ul2; if(!js) { return d; } if(s.progressive_unload && obj && obj !== -1) { obj.data("jstree_children", d); } if($.isArray(js)) { d = $(); if(!js.length) { return false; } for(i = 0, j = js.length; i < j; i++) { tmp = this._parse_json(js[i], obj, true); if(tmp.length) { d = d.add(tmp); } } } else { if(typeof js == "string") { js = { data : js }; } if(!js.data && js.data !== "") { return d; } d = $("<li />"); if(js.attr) { d.attr(js.attr); } if(js.metadata) { d.data(js.metadata); } if(js.state) { d.addClass("jstree-" + js.state); } if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } $.each(js.data, function (i, m) { tmp = $("<a />"); if($.isFunction(m)) { m = m.call(this, js); } if(typeof m == "string") { tmp.attr('href','#')[ t ? "html" : "text" ](m); } else { if(!m.attr) { m.attr = {}; } if(!m.attr.href) { m.attr.href = '#'; } tmp.attr(m.attr)[ t ? "html" : "text" ](m.title); if(m.language) { tmp.addClass(m.language); } } tmp.prepend("<ins class='jstree-icon'> </ins>"); if(!m.icon && js.icon) { m.icon = js.icon; } if(m.icon) { if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } } d.append(tmp); }); d.prepend("<ins class='jstree-icon'> </ins>"); if(js.children) { if(s.progressive_render && js.state !== "open") { d.addClass("jstree-closed").data("jstree_children", js.children); } else { if(s.progressive_unload) { d.data("jstree_children", js.children); } if($.isArray(js.children) && js.children.length) { tmp = this._parse_json(js.children, obj, true); if(tmp.length) { ul2 = $("<ul />"); ul2.append(tmp); d.append(ul2); } } } } } if(!is_callback) { ul1 = $("<ul />"); ul1.append(d); d = ul1; } return d; }, get_json : function (obj, li_attr, a_attr, is_callback) { var result = [], s = this._get_settings(), _this = this, tmp1, tmp2, li, a, t, lang; obj = this._get_node(obj); if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); } li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ]; if(!is_callback && this.data.types) { li_attr.push(s.types.type_attr); } a_attr = $.isArray(a_attr) ? a_attr : [ ]; obj.each(function () { li = $(this); tmp1 = { data : [] }; if(li_attr.length) { tmp1.attr = { }; } $.each(li_attr, function (i, v) { tmp2 = li.attr(v); if(tmp2 && tmp2.length && tmp2.replace(/jstree[^ ]*/ig,'').length) { tmp1.attr[v] = (" " + tmp2).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); } }); if(li.hasClass("jstree-open")) { tmp1.state = "open"; } if(li.hasClass("jstree-closed")) { tmp1.state = "closed"; } if(li.data()) { tmp1.metadata = li.data(); } a = li.children("a"); a.each(function () { t = $(this); if( a_attr.length || $.inArray("languages", s.plugins) !== -1 || t.children("ins").get(0).style.backgroundImage.length || (t.children("ins").get(0).className && t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').length) ) { lang = false; if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) { $.each(s.languages, function (l, lv) { if(t.hasClass(lv)) { lang = lv; return false; } }); } tmp2 = { attr : { }, title : _this.get_text(t, lang) }; $.each(a_attr, function (k, z) { tmp2.attr[z] = (" " + (t.attr(z) || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); }); if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) { $.each(s.languages, function (k, z) { if(t.hasClass(z)) { tmp2.language = z; return true; } }); } if(t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) { tmp2.icon = t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); } if(t.children("ins").get(0).style.backgroundImage.length) { tmp2.icon = t.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")",""); } } else { tmp2 = _this.get_text(t); } if(a.length > 1) { tmp1.data.push(tmp2); } else { tmp1.data = tmp2; } }); li = li.find("> ul > li"); if(li.length) { tmp1.children = _this.get_json(li, li_attr, a_attr, true); } result.push(tmp1); }); return result; } } }); })(jQuery); //*/ /* * jsTree languages plugin * Adds support for multiple language versions in one tree * This basically allows for many titles coexisting in one node, but only one of them being visible at any given time * This is useful for maintaining the same structure in many languages (hence the name of the plugin) */ (function ($) { $.jstree.plugin("languages", { __init : function () { this._load_css(); }, defaults : [], _fn : { set_lang : function (i) { var langs = this._get_settings().languages, st = false, selector = ".jstree-" + this.get_index() + ' a'; if(!$.isArray(langs) || langs.length === 0) { return false; } if($.inArray(i,langs) == -1) { if(!!langs[i]) { i = langs[i]; } else { return false; } } if(i == this.data.languages.current_language) { return true; } st = $.vakata.css.get_css(selector + "." + this.data.languages.current_language, false, this.data.languages.language_css); if(st !== false) { st.style.display = "none"; } st = $.vakata.css.get_css(selector + "." + i, false, this.data.languages.language_css); if(st !== false) { st.style.display = ""; } this.data.languages.current_language = i; this.__callback(i); return true; }, get_lang : function () { return this.data.languages.current_language; }, _get_string : function (key, lang) { var langs = this._get_settings().languages, s = this._get_settings().core.strings; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; } if(s[lang] && s[lang][key]) { return s[lang][key]; } if(s[key]) { return s[key]; } return key; }, get_text : function (obj, lang) { obj = this._get_node(obj) || this.data.ui.last_selected; if(!obj.size()) { return false; } var langs = this._get_settings().languages, s = this._get_settings().core.html_titles; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; obj = obj.children("a." + lang); } else { obj = obj.children("a:eq(0)"); } if(s) { obj = obj.clone(); obj.children("INS").remove(); return obj.html(); } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; return obj.nodeValue; } }, set_text : function (obj, val, lang) { obj = this._get_node(obj) || this.data.ui.last_selected; if(!obj.size()) { return false; } var langs = this._get_settings().languages, s = this._get_settings().core.html_titles, tmp; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; obj = obj.children("a." + lang); } else { obj = obj.children("a:eq(0)"); } if(s) { tmp = obj.children("INS").clone(); obj.html(val).prepend(tmp); this.__callback({ "obj" : obj, "name" : val, "lang" : lang }); return true; } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; this.__callback({ "obj" : obj, "name" : val, "lang" : lang }); return (obj.nodeValue = val); } }, _load_css : function () { var langs = this._get_settings().languages, str = "/* languages css */", selector = ".jstree-" + this.get_index() + ' a', ln; if($.isArray(langs) && langs.length) { this.data.languages.current_language = langs[0]; for(ln = 0; ln < langs.length; ln++) { str += selector + "." + langs[ln] + " {"; if(langs[ln] != this.data.languages.current_language) { str += " display:none; "; } str += " } "; } this.data.languages.language_css = $.vakata.css.add_sheet({ 'str' : str, 'title' : "jstree-languages" }); } }, create_node : function (obj, position, js, callback) { var t = this.__call_old(true, obj, position, js, function (t) { var langs = this._get_settings().languages, a = t.children("a"), ln; if($.isArray(langs) && langs.length) { for(ln = 0; ln < langs.length; ln++) { if(!a.is("." + langs[ln])) { t.append(a.eq(0).clone().removeClass(langs.join(" ")).addClass(langs[ln])); } } a.not("." + langs.join(", .")).remove(); } if(callback) { callback.call(this, t); } }); return t; } } }); })(jQuery); //*/ /* * jsTree cookies plugin * Stores the currently opened/selected nodes in a cookie and then restores them * Depends on the jquery.cookie plugin */ (function ($) { $.jstree.plugin("cookies", { __init : function () { if(typeof $.cookie === "undefined") { throw "jsTree cookie: jQuery cookie plugin not included."; } var s = this._get_settings().cookies, tmp; if(!!s.save_loaded) { tmp = $.cookie(s.save_loaded); if(tmp && tmp.length) { this.data.core.to_load = tmp.split(","); } } if(!!s.save_opened) { tmp = $.cookie(s.save_opened); if(tmp && tmp.length) { this.data.core.to_open = tmp.split(","); } } if(!!s.save_selected) { tmp = $.cookie(s.save_selected); if(tmp && tmp.length && this.data.ui) { this.data.ui.to_select = tmp.split(","); } } this.get_container() .one( ( this.data.ui ? "reselect" : "reopen" ) + ".jstree", $.proxy(function () { this.get_container() .bind("open_node.jstree close_node.jstree select_node.jstree deselect_node.jstree", $.proxy(function (e) { if(this._get_settings().cookies.auto_save) { this.save_cookie((e.handleObj.namespace + e.handleObj.type).replace("jstree","")); } }, this)); }, this)); }, defaults : { save_loaded : "jstree_load", save_opened : "jstree_open", save_selected : "jstree_select", auto_save : true, cookie_options : {} }, _fn : { save_cookie : function (c) { if(this.data.core.refreshing) { return; } var s = this._get_settings().cookies; if(!c) { // if called manually and not by event if(s.save_loaded) { this.save_loaded(); $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options); } if(s.save_opened) { this.save_opened(); $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options); } if(s.save_selected && this.data.ui) { this.save_selected(); $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options); } return; } switch(c) { case "open_node": case "close_node": if(!!s.save_opened) { this.save_opened(); $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options); } if(!!s.save_loaded) { this.save_loaded(); $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options); } break; case "select_node": case "deselect_node": if(!!s.save_selected && this.data.ui) { this.save_selected(); $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options); } break; } } } }); // include cookies by default // $.jstree.defaults.plugins.push("cookies"); })(jQuery); //*/ /* * jsTree sort plugin * Sorts items alphabetically (or using any other function) */ (function ($) { $.jstree.plugin("sort", { __init : function () { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var obj = this._get_node(data.rslt.obj); obj = obj === -1 ? this.get_container().children("ul") : obj.children("ul"); this.sort(obj); }, this)) .bind("rename_node.jstree create_node.jstree create.jstree", $.proxy(function (e, data) { this.sort(data.rslt.obj.parent()); }, this)) .bind("move_node.jstree", $.proxy(function (e, data) { var m = data.rslt.np == -1 ? this.get_container() : data.rslt.np; this.sort(m.children("ul")); }, this)); }, defaults : function (a, b) { return this.get_text(a) > this.get_text(b) ? 1 : -1; }, _fn : { sort : function (obj) { var s = this._get_settings().sort, t = this; obj.append($.makeArray(obj.children("li")).sort($.proxy(s, t))); obj.find("> li > ul").each(function() { t.sort($(this)); }); this.clean_node(obj); } } }); })(jQuery); //*/ /* * jsTree DND plugin * Drag and drop plugin for moving/copying nodes */ (function ($) { var o = false, r = false, m = false, ml = false, sli = false, sti = false, dir1 = false, dir2 = false, last_pos = false; $.vakata.dnd = { is_down : false, is_drag : false, helper : false, scroll_spd : 10, init_x : 0, init_y : 0, threshold : 5, helper_left : 5, helper_top : 10, user_data : {}, drag_start : function (e, data, html) { if($.vakata.dnd.is_drag) { $.vakata.drag_stop({}); } try { e.currentTarget.unselectable = "on"; e.currentTarget.onselectstart = function() { return false; }; if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; } } catch(err) { } $.vakata.dnd.init_x = e.pageX; $.vakata.dnd.init_y = e.pageY; $.vakata.dnd.user_data = data; $.vakata.dnd.is_down = true; $.vakata.dnd.helper = $("<div id='vakata-dragged' />").html(html); //.fadeTo(10,0.25); $(document).bind("mousemove", $.vakata.dnd.drag); $(document).bind("mouseup", $.vakata.dnd.drag_stop); return false; }, drag : function (e) { if(!$.vakata.dnd.is_down) { return; } if(!$.vakata.dnd.is_drag) { if(Math.abs(e.pageX - $.vakata.dnd.init_x) > 5 || Math.abs(e.pageY - $.vakata.dnd.init_y) > 5) { $.vakata.dnd.helper.appendTo("body"); $.vakata.dnd.is_drag = true; $(document).triggerHandler("drag_start.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); } else { return; } } // maybe use a scrolling parent element instead of document? if(e.type === "mousemove") { // thought of adding scroll in order to move the helper, but mouse poisition is n/a var d = $(document), t = d.scrollTop(), l = d.scrollLeft(); if(e.pageY - t < 20) { if(sti && dir1 === "down") { clearInterval(sti); sti = false; } if(!sti) { dir1 = "up"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() - $.vakata.dnd.scroll_spd); }, 150); } } else { if(sti && dir1 === "up") { clearInterval(sti); sti = false; } } if($(window).height() - (e.pageY - t) < 20) { if(sti && dir1 === "up") { clearInterval(sti); sti = false; } if(!sti) { dir1 = "down"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() + $.vakata.dnd.scroll_spd); }, 150); } } else { if(sti && dir1 === "down") { clearInterval(sti); sti = false; } } if(e.pageX - l < 20) { if(sli && dir2 === "right") { clearInterval(sli); sli = false; } if(!sli) { dir2 = "left"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() - $.vakata.dnd.scroll_spd); }, 150); } } else { if(sli && dir2 === "left") { clearInterval(sli); sli = false; } } if($(window).width() - (e.pageX - l) < 20) { if(sli && dir2 === "left") { clearInterval(sli); sli = false; } if(!sli) { dir2 = "right"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() + $.vakata.dnd.scroll_spd); }, 150); } } else { if(sli && dir2 === "right") { clearInterval(sli); sli = false; } } } $.vakata.dnd.helper.css({ left : (e.pageX + $.vakata.dnd.helper_left) + "px", top : (e.pageY + $.vakata.dnd.helper_top) + "px" }); $(document).triggerHandler("drag.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); }, drag_stop : function (e) { if(sli) { clearInterval(sli); } if(sti) { clearInterval(sti); } $(document).unbind("mousemove", $.vakata.dnd.drag); $(document).unbind("mouseup", $.vakata.dnd.drag_stop); $(document).triggerHandler("drag_stop.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); $.vakata.dnd.helper.remove(); $.vakata.dnd.init_x = 0; $.vakata.dnd.init_y = 0; $.vakata.dnd.user_data = {}; $.vakata.dnd.is_down = false; $.vakata.dnd.is_drag = false; } }; $(function() { var css_string = '#vakata-dragged { display:block; margin:0 0 0 0; padding:4px 4px 4px 24px; position:absolute; top:-2000px; line-height:16px; z-index:10000; } '; $.vakata.css.add_sheet({ str : css_string, title : "vakata" }); }); $.jstree.plugin("dnd", { __init : function () { this.data.dnd = { active : false, after : false, inside : false, before : false, off : false, prepared : false, w : 0, to1 : false, to2 : false, cof : false, cw : false, ch : false, i1 : false, i2 : false, mto : false }; this.get_container() .bind("mouseenter.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(this.data.themes) { m.attr("class", "jstree-" + this.data.themes.theme); if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } //if($(e.currentTarget).find("> ul > li").length === 0) { if(e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree var tr = $.jstree._reference(e.target), dc; if(tr.data.dnd.foreign) { dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); if(dc === true || dc.inside === true || dc.before === true || dc.after === true) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } } else { tr.prepare_move(o, tr.get_container(), "last"); if(tr.check_move()) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } } } } }, this)) .bind("mouseup.jstree", $.proxy(function (e) { //if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && $(e.currentTarget).find("> ul > li").length === 0) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree var tr = $.jstree._reference(e.currentTarget), dc; if(tr.data.dnd.foreign) { dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); if(dc === true || dc.inside === true || dc.before === true || dc.after === true) { tr._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); } } else { tr.move_node(o, tr.get_container(), "last", e[tr._get_settings().dnd.copy_modifier + "Key"]); } } }, this)) .bind("mouseleave.jstree", $.proxy(function (e) { if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") { return false; } if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if($.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } } }, this)) .bind("mousemove.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { var cnt = this.get_container()[0]; // Horizontal scroll if(e.pageX + 24 > this.data.dnd.cof.left + this.data.dnd.cw) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft += $.vakata.dnd.scroll_spd; }, cnt), 100); } else if(e.pageX - 24 < this.data.dnd.cof.left) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft -= $.vakata.dnd.scroll_spd; }, cnt), 100); } else { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } } // Vertical scroll if(e.pageY + 24 > this.data.dnd.cof.top + this.data.dnd.ch) { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop += $.vakata.dnd.scroll_spd; }, cnt), 100); } else if(e.pageY - 24 < this.data.dnd.cof.top) { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop -= $.vakata.dnd.scroll_spd; }, cnt), 100); } else { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } } } }, this)) .bind("scroll.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && m && ml) { m.hide(); ml.hide(); } }, this)) .delegate("a", "mousedown.jstree", $.proxy(function (e) { if(e.which === 1) { this.start_drag(e.currentTarget, e); return false; } }, this)) .delegate("a", "mouseenter.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { this.dnd_enter(e.currentTarget); } }, this)) .delegate("a", "mousemove.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(!r || !r.length || r.children("a")[0] !== e.currentTarget) { this.dnd_enter(e.currentTarget); } if(typeof this.data.dnd.off.top === "undefined") { this.data.dnd.off = $(e.target).offset(); } this.data.dnd.w = (e.pageY - (this.data.dnd.off.top || 0)) % this.data.core.li_height; if(this.data.dnd.w < 0) { this.data.dnd.w += this.data.core.li_height; } this.dnd_show(); } }, this)) .delegate("a", "mouseleave.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") { return false; } if(m) { m.hide(); } if(ml) { ml.hide(); } /* var ec = $(e.currentTarget).closest("li"), er = $(e.relatedTarget).closest("li"); if(er[0] !== ec.prev()[0] && er[0] !== ec.next()[0]) { if(m) { m.hide(); } if(ml) { ml.hide(); } } */ this.data.dnd.mto = setTimeout( (function (t) { return function () { t.dnd_leave(e); }; })(this), 0); } }, this)) .delegate("a", "mouseup.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { this.dnd_finish(e); } }, this)); $(document) .bind("drag_stop.vakata", $.proxy(function () { if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.after = false; this.data.dnd.before = false; this.data.dnd.inside = false; this.data.dnd.off = false; this.data.dnd.prepared = false; this.data.dnd.w = false; this.data.dnd.to1 = false; this.data.dnd.to2 = false; this.data.dnd.i1 = false; this.data.dnd.i2 = false; this.data.dnd.active = false; this.data.dnd.foreign = false; if(m) { m.css({ "top" : "-2000px" }); } if(ml) { ml.css({ "top" : "-2000px" }); } }, this)) .bind("drag_start.vakata", $.proxy(function (e, data) { if(data.data.jstree) { var et = $(data.event.target); if(et.closest(".jstree").hasClass("jstree-" + this.get_index())) { this.dnd_enter(et); } } }, this)); /* .bind("keydown.jstree-" + this.get_index() + " keyup.jstree-" + this.get_index(), $.proxy(function(e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && !this.data.dnd.foreign) { var h = $.vakata.dnd.helper.children("ins"); if(e[this._get_settings().dnd.copy_modifier + "Key"] && h.hasClass("jstree-ok")) { h.parent().html(h.parent().html().replace(/ \(Copy\)$/, "") + " (Copy)"); } else { h.parent().html(h.parent().html().replace(/ \(Copy\)$/, "")); } } }, this)); */ var s = this._get_settings().dnd; if(s.drag_target) { $(document) .delegate(s.drag_target, "mousedown.jstree-" + this.get_index(), $.proxy(function (e) { o = e.target; $.vakata.dnd.drag_start(e, { jstree : true, obj : e.target }, "<ins class='jstree-icon'></ins>" + $(e.target).text() ); if(this.data.themes) { if(m) { m.attr("class", "jstree-" + this.data.themes.theme); } if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); var cnt = this.get_container(); this.data.dnd.cof = cnt.offset(); this.data.dnd.cw = parseInt(cnt.width(),10); this.data.dnd.ch = parseInt(cnt.height(),10); this.data.dnd.foreign = true; e.preventDefault(); }, this)); } if(s.drop_target) { $(document) .delegate(s.drop_target, "mouseenter.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active && this._get_settings().dnd.drop_check.call(this, { "o" : o, "r" : $(e.target), "e" : e })) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } }, this)) .delegate(s.drop_target, "mouseleave.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } }, this)) .delegate(s.drop_target, "mouseup.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active && $.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) { this._get_settings().dnd.drop_finish.call(this, { "o" : o, "r" : $(e.target), "e" : e }); } }, this)); } }, defaults : { copy_modifier : "ctrl", check_timeout : 100, open_timeout : 500, drop_target : ".jstree-drop", drop_check : function (data) { return true; }, drop_finish : $.noop, drag_target : ".jstree-draggable", drag_finish : $.noop, drag_check : function (data) { return { after : false, before : false, inside : true }; } }, _fn : { dnd_prepare : function () { if(!r || !r.length) { return; } this.data.dnd.off = r.offset(); if(this._get_settings().core.rtl) { this.data.dnd.off.right = this.data.dnd.off.left + r.width(); } if(this.data.dnd.foreign) { var a = this._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : r }); this.data.dnd.after = a.after; this.data.dnd.before = a.before; this.data.dnd.inside = a.inside; this.data.dnd.prepared = true; return this.dnd_show(); } this.prepare_move(o, r, "before"); this.data.dnd.before = this.check_move(); this.prepare_move(o, r, "after"); this.data.dnd.after = this.check_move(); if(this._is_loaded(r)) { this.prepare_move(o, r, "inside"); this.data.dnd.inside = this.check_move(); } else { this.data.dnd.inside = false; } this.data.dnd.prepared = true; return this.dnd_show(); }, dnd_show : function () { if(!this.data.dnd.prepared) { return; } var o = ["before","inside","after"], r = false, rtl = this._get_settings().core.rtl, pos; if(this.data.dnd.w < this.data.core.li_height/3) { o = ["before","inside","after"]; } else if(this.data.dnd.w <= this.data.core.li_height*2/3) { o = this.data.dnd.w < this.data.core.li_height/2 ? ["inside","before","after"] : ["inside","after","before"]; } else { o = ["after","inside","before"]; } $.each(o, $.proxy(function (i, val) { if(this.data.dnd[val]) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); r = val; return false; } }, this)); if(r === false) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } pos = rtl ? (this.data.dnd.off.right - 18) : (this.data.dnd.off.left + 10); switch(r) { case "before": m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top - 6) + "px" }).show(); if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top - 1) + "px" }).show(); } break; case "after": m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 6) + "px" }).show(); if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 1) + "px" }).show(); } break; case "inside": m.css({ "left" : pos + ( rtl ? -4 : 4) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height/2 - 5) + "px" }).show(); if(ml) { ml.hide(); } break; default: m.hide(); if(ml) { ml.hide(); } break; } last_pos = r; return r; }, dnd_open : function () { this.data.dnd.to2 = false; this.open_node(r, $.proxy(this.dnd_prepare,this), true); }, dnd_finish : function (e) { if(this.data.dnd.foreign) { if(this.data.dnd.after || this.data.dnd.before || this.data.dnd.inside) { this._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : r, "p" : last_pos }); } } else { this.dnd_prepare(); this.move_node(o, r, last_pos, e[this._get_settings().dnd.copy_modifier + "Key"]); } o = false; r = false; m.hide(); if(ml) { ml.hide(); } }, dnd_enter : function (obj) { if(this.data.dnd.mto) { clearTimeout(this.data.dnd.mto); this.data.dnd.mto = false; } var s = this._get_settings().dnd; this.data.dnd.prepared = false; r = this._get_node(obj); if(s.check_timeout) { // do the calculations after a minimal timeout (users tend to drag quickly to the desired location) if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } this.data.dnd.to1 = setTimeout($.proxy(this.dnd_prepare, this), s.check_timeout); } else { this.dnd_prepare(); } if(s.open_timeout) { if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if(r && r.length && r.hasClass("jstree-closed")) { // if the node is closed - open it, then recalculate this.data.dnd.to2 = setTimeout($.proxy(this.dnd_open, this), s.open_timeout); } } else { if(r && r.length && r.hasClass("jstree-closed")) { this.dnd_open(); } } }, dnd_leave : function (e) { this.data.dnd.after = false; this.data.dnd.before = false; this.data.dnd.inside = false; $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); m.hide(); if(ml) { ml.hide(); } if(r && r[0] === e.target.parentNode) { if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); this.data.dnd.to1 = false; } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); this.data.dnd.to2 = false; } } }, start_drag : function (obj, e) { o = this._get_node(obj); if(this.data.ui && this.is_selected(o)) { o = this._get_node(null, true); } var dt = o.length > 1 ? this._get_string("multiple_selection") : this.get_text(o), cnt = this.get_container(); if(!this._get_settings().core.html_titles) { dt = dt.replace(/</ig,"<").replace(/>/ig,">"); } $.vakata.dnd.drag_start(e, { jstree : true, obj : o }, "<ins class='jstree-icon'></ins>" + dt ); if(this.data.themes) { if(m) { m.attr("class", "jstree-" + this.data.themes.theme); } if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } this.data.dnd.cof = cnt.offset(); this.data.dnd.cw = parseInt(cnt.width(),10); this.data.dnd.ch = parseInt(cnt.height(),10); this.data.dnd.active = true; } } }); $(function() { var css_string = '' + '#vakata-dragged ins { display:block; text-decoration:none; width:16px; height:16px; margin:0 0 0 0; padding:0; position:absolute; top:4px; left:4px; ' + ' -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; ' + '} ' + '#vakata-dragged .jstree-ok { background:green; } ' + '#vakata-dragged .jstree-invalid { background:red; } ' + '#jstree-marker { padding:0; margin:0; font-size:12px; overflow:hidden; height:12px; width:8px; position:absolute; top:-30px; z-index:10001; background-repeat:no-repeat; display:none; background-color:transparent; text-shadow:1px 1px 1px white; color:black; line-height:10px; } ' + '#jstree-marker-line { padding:0; margin:0; line-height:0%; font-size:1px; overflow:hidden; height:1px; width:100px; position:absolute; top:-30px; z-index:10000; background-repeat:no-repeat; display:none; background-color:#456c43; ' + ' cursor:pointer; border:1px solid #eeeeee; border-left:0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; ' + ' -moz-border-radius:1px; border-radius:1px; -webkit-border-radius:1px; ' + '}' + ''; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); m = $("<div />").attr({ id : "jstree-marker" }).hide().html("»") .bind("mouseleave mouseenter", function (e) { m.hide(); ml.hide(); e.preventDefault(); e.stopImmediatePropagation(); return false; }) .appendTo("body"); ml = $("<div />").attr({ id : "jstree-marker-line" }).hide() .bind("mouseup", function (e) { if(r && r.length) { r.children("a").trigger(e); e.preventDefault(); e.stopImmediatePropagation(); return false; } }) .bind("mouseleave", function (e) { var rt = $(e.relatedTarget); if(rt.is(".jstree") || rt.closest(".jstree").length === 0) { if(r && r.length) { r.children("a").trigger(e); m.hide(); ml.hide(); e.preventDefault(); e.stopImmediatePropagation(); return false; } } }) .appendTo("body"); $(document).bind("drag_start.vakata", function (e, data) { if(data.data.jstree) { m.show(); if(ml) { ml.show(); } } }); $(document).bind("drag_stop.vakata", function (e, data) { if(data.data.jstree) { m.hide(); if(ml) { ml.hide(); } } }); }); })(jQuery); //*/ /* * jsTree checkbox plugin * Inserts checkboxes in front of every node * Depends on the ui plugin * DOES NOT WORK NICELY WITH MULTITREE DRAG'N'DROP */ (function ($) { $.jstree.plugin("checkbox", { __init : function () { this.data.checkbox.noui = this._get_settings().checkbox.override_ui; if(this.data.ui && this.data.checkbox.noui) { this.select_node = this.deselect_node = this.deselect_all = $.noop; this.get_selected = this.get_checked; } this.get_container() .bind("open_node.jstree create_node.jstree clean_node.jstree refresh.jstree", $.proxy(function (e, data) { this._prepare_checkboxes(data.rslt.obj); }, this)) .bind("loaded.jstree", $.proxy(function (e) { this._prepare_checkboxes(); }, this)) .delegate( (this.data.ui && this.data.checkbox.noui ? "a" : "ins.jstree-checkbox") , "click.jstree", $.proxy(function (e) { e.preventDefault(); if(this._get_node(e.target).hasClass("jstree-checked")) { this.uncheck_node(e.target); } else { this.check_node(e.target); } if(this.data.ui && this.data.checkbox.noui) { this.save_selected(); if(this.data.cookies) { this.save_cookie("select_node"); } } else { e.stopImmediatePropagation(); return false; } }, this)); }, defaults : { override_ui : false, two_state : false, real_checkboxes : false, checked_parent_open : true, real_checkboxes_names : function (n) { return [ ("check_" + (n[0].id || Math.ceil(Math.random() * 10000))) , 1]; } }, __destroy : function () { this.get_container() .find("input.jstree-real-checkbox").removeClass("jstree-real-checkbox").end() .find("ins.jstree-checkbox").remove(); }, _fn : { _checkbox_notify : function (n, data) { if(data.checked) { this.check_node(n, false); } }, _prepare_checkboxes : function (obj) { obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj); if(obj === false) { return; } // added for removing root nodes var c, _this = this, t, ts = this._get_settings().checkbox.two_state, rc = this._get_settings().checkbox.real_checkboxes, rcn = this._get_settings().checkbox.real_checkboxes_names; obj.each(function () { t = $(this); c = t.is("li") && (t.hasClass("jstree-checked") || (rc && t.children(":checked").length)) ? "jstree-checked" : "jstree-unchecked"; t.find("li").andSelf().each(function () { var $t = $(this), nm; $t.children("a" + (_this.data.languages ? "" : ":eq(0)") ).not(":has(.jstree-checkbox)").prepend("<ins class='jstree-checkbox'> </ins>").parent().not(".jstree-checked, .jstree-unchecked").addClass( ts ? "jstree-unchecked" : c ); if(rc) { if(!$t.children(":checkbox").length) { nm = rcn.call(_this, $t); $t.prepend("<input type='checkbox' class='jstree-real-checkbox' id='" + nm[0] + "' name='" + nm[0] + "' value='" + nm[1] + "' />"); } else { $t.children(":checkbox").addClass("jstree-real-checkbox"); } } if(!ts) { if(c === "jstree-checked" || $t.hasClass("jstree-checked") || $t.children(':checked').length) { $t.find("li").andSelf().addClass("jstree-checked").children(":checkbox").prop("checked", true); } } else { if($t.hasClass("jstree-checked") || $t.children(':checked').length) { $t.addClass("jstree-checked").children(":checkbox").prop("checked", true); } } }); }); if(!ts) { obj.find(".jstree-checked").parent().parent().each(function () { _this._repair_state(this); }); } }, change_state : function (obj, state) { obj = this._get_node(obj); var coll = false, rc = this._get_settings().checkbox.real_checkboxes; if(!obj || obj === -1) { return false; } state = (state === false || state === true) ? state : obj.hasClass("jstree-checked"); if(this._get_settings().checkbox.two_state) { if(state) { obj.removeClass("jstree-checked").addClass("jstree-unchecked"); if(rc) { obj.children(":checkbox").prop("checked", false); } } else { obj.removeClass("jstree-unchecked").addClass("jstree-checked"); if(rc) { obj.children(":checkbox").prop("checked", true); } } } else { if(state) { coll = obj.find("li").andSelf(); if(!coll.filter(".jstree-checked, .jstree-undetermined").length) { return false; } coll.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked"); if(rc) { coll.children(":checkbox").prop("checked", false); } } else { coll = obj.find("li").andSelf(); if(!coll.filter(".jstree-unchecked, .jstree-undetermined").length) { return false; } coll.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked"); if(rc) { coll.children(":checkbox").prop("checked", true); } if(this.data.ui) { this.data.ui.last_selected = obj; } this.data.checkbox.last_selected = obj; } obj.parentsUntil(".jstree", "li").each(function () { var $this = $(this); if(state) { if($this.children("ul").children("li.jstree-checked, li.jstree-undetermined").length) { $this.parentsUntil(".jstree", "li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { $this.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } return false; } else { $this.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked"); if(rc) { $this.children(":checkbox").prop("checked", false); } } } else { if($this.children("ul").children("li.jstree-unchecked, li.jstree-undetermined").length) { $this.parentsUntil(".jstree", "li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { $this.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } return false; } else { $this.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked"); if(rc) { $this.children(":checkbox").prop("checked", true); } } } }); } if(this.data.ui && this.data.checkbox.noui) { this.data.ui.selected = this.get_checked(); } this.__callback(obj); return true; }, check_node : function (obj) { if(this.change_state(obj, false)) { obj = this._get_node(obj); if(this._get_settings().checkbox.checked_parent_open) { var t = this; obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); } this.__callback({ "obj" : obj }); } }, uncheck_node : function (obj) { if(this.change_state(obj, true)) { this.__callback({ "obj" : this._get_node(obj) }); } }, check_all : function () { var _this = this, coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li"); coll.each(function () { _this.change_state(this, false); }); this.__callback(); }, uncheck_all : function () { var _this = this, coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li"); coll.each(function () { _this.change_state(this, true); }); this.__callback(); }, is_checked : function(obj) { obj = this._get_node(obj); return obj.length ? obj.is(".jstree-checked") : false; }, get_checked : function (obj, get_all) { obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj); return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-checked") : obj.find("> ul > .jstree-checked, .jstree-undetermined > ul > .jstree-checked"); }, get_unchecked : function (obj, get_all) { obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj); return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-unchecked") : obj.find("> ul > .jstree-unchecked, .jstree-undetermined > ul > .jstree-unchecked"); }, show_checkboxes : function () { this.get_container().children("ul").removeClass("jstree-no-checkboxes"); }, hide_checkboxes : function () { this.get_container().children("ul").addClass("jstree-no-checkboxes"); }, _repair_state : function (obj) { obj = this._get_node(obj); if(!obj.length) { return; } if(this._get_settings().checkbox.two_state) { obj.find('li').andSelf().not('.jstree-checked').removeClass('jstree-undetermined').addClass('jstree-unchecked').children(':checkbox').prop('checked', true); return; } var rc = this._get_settings().checkbox.real_checkboxes, a = obj.find("> ul > .jstree-checked").length, b = obj.find("> ul > .jstree-undetermined").length, c = obj.find("> ul > li").length; if(c === 0) { if(obj.hasClass("jstree-undetermined")) { this.change_state(obj, false); } } else if(a === 0 && b === 0) { this.change_state(obj, true); } else if(a === c) { this.change_state(obj, false); } else { obj.parentsUntil(".jstree","li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { obj.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } } }, reselect : function () { if(this.data.ui && this.data.checkbox.noui) { var _this = this, s = this.data.ui.to_select; s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); this.deselect_all(); $.each(s, function (i, val) { _this.check_node(val); }); this.__callback(); } else { this.__call_old(); } }, save_loaded : function () { var _this = this; this.data.core.to_load = []; this.get_container_ul().find("li.jstree-closed.jstree-undetermined").each(function () { if(this.id) { _this.data.core.to_load.push("#" + this.id); } }); } } }); $(function() { var css_string = '.jstree .jstree-real-checkbox { display:none; } '; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree XML plugin * The XML data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions. */ (function ($) { $.vakata.xslt = function (xml, xsl, callback) { var rs = "", xm, xs, processor, support; // TODO: IE9 no XSLTProcessor, no document.recalc if(document.recalc) { xm = document.createElement('xml'); xs = document.createElement('xml'); xm.innerHTML = xml; xs.innerHTML = xsl; $("body").append(xm).append(xs); setTimeout( (function (xm, xs, callback) { return function () { callback.call(null, xm.transformNode(xs.XMLDocument)); setTimeout( (function (xm, xs) { return function () { $(xm).remove(); $(xs).remove(); }; })(xm, xs), 200); }; })(xm, xs, callback), 100); return true; } if(typeof window.DOMParser !== "undefined" && typeof window.XMLHttpRequest !== "undefined" && typeof window.XSLTProcessor === "undefined") { xml = new DOMParser().parseFromString(xml, "text/xml"); xsl = new DOMParser().parseFromString(xsl, "text/xml"); // alert(xml.transformNode()); // callback.call(null, new XMLSerializer().serializeToString(rs)); } if(typeof window.DOMParser !== "undefined" && typeof window.XMLHttpRequest !== "undefined" && typeof window.XSLTProcessor !== "undefined") { processor = new XSLTProcessor(); support = $.isFunction(processor.transformDocument) ? (typeof window.XMLSerializer !== "undefined") : true; if(!support) { return false; } xml = new DOMParser().parseFromString(xml, "text/xml"); xsl = new DOMParser().parseFromString(xsl, "text/xml"); if($.isFunction(processor.transformDocument)) { rs = document.implementation.createDocument("", "", null); processor.transformDocument(xml, xsl, rs, null); callback.call(null, new XMLSerializer().serializeToString(rs)); return true; } else { processor.importStylesheet(xsl); rs = processor.transformToFragment(xml, document); callback.call(null, $("<div />").append(rs).html()); return true; } } return false; }; var xsl = { 'nest' : '<' + '?xml version="1.0" encoding="utf-8" ?>' + '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' + '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/html" />' + '<xsl:template match="/">' + ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="/root" />' + ' </xsl:call-template>' + '</xsl:template>' + '<xsl:template name="nodes">' + ' <xsl:param name="node" />' + ' <ul>' + ' <xsl:for-each select="$node/item">' + ' <xsl:variable name="children" select="count(./item) > 0" />' + ' <li>' + ' <xsl:attribute name="class">' + ' <xsl:if test="position() = last()">jstree-last </xsl:if>' + ' <xsl:choose>' + ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' + ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' + ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' + ' </xsl:choose>' + ' <xsl:value-of select="@class" />' + ' </xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'class\' and name() != \'state\' and name() != \'hasChildren\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins class="jstree-icon"><xsl:text> </xsl:text></ins>' + ' <xsl:for-each select="content/name">' + ' <a>' + ' <xsl:attribute name="href">' + ' <xsl:choose>' + ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' + ' <xsl:otherwise>#</xsl:otherwise>' + ' </xsl:choose>' + ' </xsl:attribute>' + ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' + ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins>' + ' <xsl:attribute name="class">jstree-icon ' + ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' + ' </xsl:attribute>' + ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' + ' <xsl:text> </xsl:text>' + ' </ins>' + ' <xsl:copy-of select="./child::node()" />' + ' </a>' + ' </xsl:for-each>' + ' <xsl:if test="$children or @hasChildren"><xsl:call-template name="nodes"><xsl:with-param name="node" select="current()" /></xsl:call-template></xsl:if>' + ' </li>' + ' </xsl:for-each>' + ' </ul>' + '</xsl:template>' + '</xsl:stylesheet>', 'flat' : '<' + '?xml version="1.0" encoding="utf-8" ?>' + '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' + '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/xml" />' + '<xsl:template match="/">' + ' <ul>' + ' <xsl:for-each select="//item[not(@parent_id) or @parent_id=0 or not(@parent_id = //item/@id)]">' + /* the last `or` may be removed */ ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="." />' + ' <xsl:with-param name="is_last" select="number(position() = last())" />' + ' </xsl:call-template>' + ' </xsl:for-each>' + ' </ul>' + '</xsl:template>' + '<xsl:template name="nodes">' + ' <xsl:param name="node" />' + ' <xsl:param name="is_last" />' + ' <xsl:variable name="children" select="count(//item[@parent_id=$node/attribute::id]) > 0" />' + ' <li>' + ' <xsl:attribute name="class">' + ' <xsl:if test="$is_last = true()">jstree-last </xsl:if>' + ' <xsl:choose>' + ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' + ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' + ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' + ' </xsl:choose>' + ' <xsl:value-of select="@class" />' + ' </xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'parent_id\' and name() != \'hasChildren\' and name() != \'class\' and name() != \'state\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins class="jstree-icon"><xsl:text> </xsl:text></ins>' + ' <xsl:for-each select="content/name">' + ' <a>' + ' <xsl:attribute name="href">' + ' <xsl:choose>' + ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' + ' <xsl:otherwise>#</xsl:otherwise>' + ' </xsl:choose>' + ' </xsl:attribute>' + ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' + ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins>' + ' <xsl:attribute name="class">jstree-icon ' + ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' + ' </xsl:attribute>' + ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' + ' <xsl:text> </xsl:text>' + ' </ins>' + ' <xsl:copy-of select="./child::node()" />' + ' </a>' + ' </xsl:for-each>' + ' <xsl:if test="$children">' + ' <ul>' + ' <xsl:for-each select="//item[@parent_id=$node/attribute::id]">' + ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="." />' + ' <xsl:with-param name="is_last" select="number(position() = last())" />' + ' </xsl:call-template>' + ' </xsl:for-each>' + ' </ul>' + ' </xsl:if>' + ' </li>' + '</xsl:template>' + '</xsl:stylesheet>' }, escape_xml = function(string) { return string .toString() .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; $.jstree.plugin("xml_data", { defaults : { data : false, ajax : false, xsl : "flat", clean_node : false, correct_state : true, get_skip_empty : false, get_include_preamble : true }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_xml(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { var s = this._get_settings().xml_data; obj = this._get_node(obj); return obj == -1 || !obj || (!s.ajax && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0; }, load_node_xml : function (obj, s_call, e_call) { var s = this.get_settings().xml_data, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied."; case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { this.parse_xml(d, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); } if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } } }, this)); }, this)); break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { this.parse_xml(s.data, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); this.get_container().children("ul").empty().append(d.children()); if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } }, this)); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): error_func = function (x, t, e) { var ef = this.get_settings().xml_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj !== -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { d = x.responseText; var sf = this.get_settings().xml_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) { return error_func.call(this, x, t, ""); } this.parse_xml(d, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); } if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } } }, this)); }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "xml"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } }, parse_xml : function (xml, callback) { var s = this._get_settings().xml_data; $.vakata.xslt(xml, xsl[s.xsl], callback); }, get_xml : function (tp, obj, li_attr, a_attr, is_callback) { var result = "", s = this._get_settings(), _this = this, tmp1, tmp2, li, a, lang; if(!tp) { tp = "flat"; } if(!is_callback) { is_callback = 0; } obj = this._get_node(obj); if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); } li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ]; if(!is_callback && this.data.types && $.inArray(s.types.type_attr, li_attr) === -1) { li_attr.push(s.types.type_attr); } a_attr = $.isArray(a_attr) ? a_attr : [ ]; if(!is_callback) { if(s.xml_data.get_include_preamble) { result += '<' + '?xml version="1.0" encoding="UTF-8"?' + '>'; } result += "<root>"; } obj.each(function () { result += "<item"; li = $(this); $.each(li_attr, function (i, v) { var t = li.attr(v); if(!s.xml_data.get_skip_empty || typeof t !== "undefined") { result += " " + v + "=\"" + escape_xml((" " + (t || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\""; } }); if(li.hasClass("jstree-open")) { result += " state=\"open\""; } if(li.hasClass("jstree-closed")) { result += " state=\"closed\""; } if(tp === "flat") { result += " parent_id=\"" + escape_xml(is_callback) + "\""; } result += ">"; result += "<content>"; a = li.children("a"); a.each(function () { tmp1 = $(this); lang = false; result += "<name"; if($.inArray("languages", s.plugins) !== -1) { $.each(s.languages, function (k, z) { if(tmp1.hasClass(z)) { result += " lang=\"" + escape_xml(z) + "\""; lang = z; return false; } }); } if(a_attr.length) { $.each(a_attr, function (k, z) { var t = tmp1.attr(z); if(!s.xml_data.get_skip_empty || typeof t !== "undefined") { result += " " + z + "=\"" + escape_xml((" " + t || "").replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\""; } }); } if(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) { result += ' icon="' + escape_xml(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + '"'; } if(tmp1.children("ins").get(0).style.backgroundImage.length) { result += ' icon="' + escape_xml(tmp1.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")","").replace(/'/ig,"").replace(/"/ig,"")) + '"'; } result += ">"; result += "<![CDATA[" + _this.get_text(tmp1, lang) + "]]>"; result += "</name>"; }); result += "</content>"; tmp2 = li[0].id || true; li = li.find("> ul > li"); if(li.length) { tmp2 = _this.get_xml(tp, li, li_attr, a_attr, tmp2); } else { tmp2 = ""; } if(tp == "nest") { result += tmp2; } result += "</item>"; if(tp == "flat") { result += tmp2; } }); if(!is_callback) { result += "</root>"; } return result; } } }); })(jQuery); //*/ /* * jsTree search plugin * Enables both sync and async search on the tree * DOES NOT WORK WITH JSON PROGRESSIVE RENDER */ (function ($) { $.expr[':'].jstree_contains = function(a,i,m){ return (a.textContent || a.innerText || "").toLowerCase().indexOf(m[3].toLowerCase())>=0; }; $.expr[':'].jstree_title_contains = function(a,i,m) { return (a.getAttribute("title") || "").toLowerCase().indexOf(m[3].toLowerCase())>=0; }; $.jstree.plugin("search", { __init : function () { this.data.search.str = ""; this.data.search.result = $(); if(this._get_settings().search.show_only_matches) { this.get_container() .bind("search.jstree", function (e, data) { $(this).children("ul").find("li").hide().removeClass("jstree-last"); data.rslt.nodes.parentsUntil(".jstree").andSelf().show() .filter("ul").each(function () { $(this).children("li:visible").eq(-1).addClass("jstree-last"); }); }) .bind("clear_search.jstree", function () { $(this).children("ul").find("li").css("display","").end().end().jstree("clean_node", -1); }); } }, defaults : { ajax : false, search_method : "jstree_contains", // for case insensitive - jstree_contains show_only_matches : false }, _fn : { search : function (str, skip_async) { if($.trim(str) === "") { this.clear_search(); return; } var s = this.get_settings().search, t = this, error_func = function () { }, success_func = function () { }; this.data.search.str = str; if(!skip_async && s.ajax !== false && this.get_container_ul().find("li.jstree-closed:not(:has(ul)):eq(0)").length > 0) { this.search.supress_callback = true; error_func = function () { }; success_func = function (d, t, x) { var sf = this.get_settings().search.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } this.data.search.to_open = d; this._search_open(); }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, str); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, str); } if(!s.ajax.data) { s.ajax.data = { "search_string" : str }; } if(!s.ajax.dataType || /^json/.exec(s.ajax.dataType)) { s.ajax.dataType = "json"; } $.ajax(s.ajax); return; } if(this.data.search.result.length) { this.clear_search(); } this.data.search.result = this.get_container().find("a" + (this.data.languages ? "." + this.get_lang() : "" ) + ":" + (s.search_method) + "(" + this.data.search.str + ")"); this.data.search.result.addClass("jstree-search").parent().parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); this.__callback({ nodes : this.data.search.result, str : str }); }, clear_search : function (str) { this.data.search.result.removeClass("jstree-search"); this.__callback(this.data.search.result); this.data.search.result = $(); }, _search_open : function (is_callback) { var _this = this, done = true, current = [], remaining = []; if(this.data.search.to_open.length) { $.each(this.data.search.to_open, function (i, val) { if(val == "#") { return true; } if($(val).length && $(val).is(".jstree-closed")) { current.push(val); } else { remaining.push(val); } }); if(current.length) { this.data.search.to_open = remaining; $.each(current, function (i, val) { _this.open_node(val, function () { _this._search_open(true); }); }); done = false; } } if(done) { this.search(this.data.search.str, true); } } } }); })(jQuery); //*/ /* * jsTree contextmenu plugin */ (function ($) { $.vakata.context = { hide_on_mouseleave : false, cnt : $("<div id='vakata-contextmenu' />"), vis : false, tgt : false, par : false, func : false, data : false, rtl : false, show : function (s, t, x, y, d, p, rtl) { $.vakata.context.rtl = !!rtl; var html = $.vakata.context.parse(s), h, w; if(!html) { return; } $.vakata.context.vis = true; $.vakata.context.tgt = t; $.vakata.context.par = p || t || null; $.vakata.context.data = d || null; $.vakata.context.cnt .html(html) .css({ "visibility" : "hidden", "display" : "block", "left" : 0, "top" : 0 }); if($.vakata.context.hide_on_mouseleave) { $.vakata.context.cnt .one("mouseleave", function(e) { $.vakata.context.hide(); }); } h = $.vakata.context.cnt.height(); w = $.vakata.context.cnt.width(); if(x + w > $(document).width()) { x = $(document).width() - (w + 5); $.vakata.context.cnt.find("li > ul").addClass("right"); } if(y + h > $(document).height()) { y = y - (h + t[0].offsetHeight); $.vakata.context.cnt.find("li > ul").addClass("bottom"); } $.vakata.context.cnt .css({ "left" : x, "top" : y }) .find("li:has(ul)") .bind("mouseenter", function (e) { var w = $(document).width(), h = $(document).height(), ul = $(this).children("ul").show(); if(w !== $(document).width()) { ul.toggleClass("right"); } if(h !== $(document).height()) { ul.toggleClass("bottom"); } }) .bind("mouseleave", function (e) { $(this).children("ul").hide(); }) .end() .css({ "visibility" : "visible" }) .show(); $(document).triggerHandler("context_show.vakata"); }, hide : function () { $.vakata.context.vis = false; $.vakata.context.cnt.attr("class","").css({ "visibility" : "hidden" }); $(document).triggerHandler("context_hide.vakata"); }, parse : function (s, is_callback) { if(!s) { return false; } var str = "", tmp = false, was_sep = true; if(!is_callback) { $.vakata.context.func = {}; } str += "<ul>"; $.each(s, function (i, val) { if(!val) { return true; } $.vakata.context.func[i] = val.action; if(!was_sep && val.separator_before) { str += "<li class='vakata-separator vakata-separator-before'></li>"; } was_sep = false; str += "<li class='" + (val._class || "") + (val._disabled ? " jstree-contextmenu-disabled " : "") + "'><ins "; if(val.icon && val.icon.indexOf("/") === -1) { str += " class='" + val.icon + "' "; } if(val.icon && val.icon.indexOf("/") !== -1) { str += " style='background:url(" + val.icon + ") center center no-repeat;' "; } str += "> </ins><a href='#' rel='" + i + "'>"; if(val.submenu) { str += "<span style='float:" + ($.vakata.context.rtl ? "left" : "right") + ";'>»</span>"; } str += val.label + "</a>"; if(val.submenu) { tmp = $.vakata.context.parse(val.submenu, true); if(tmp) { str += tmp; } } str += "</li>"; if(val.separator_after) { str += "<li class='vakata-separator vakata-separator-after'></li>"; was_sep = true; } }); str = str.replace(/<li class\='vakata-separator vakata-separator-after'\><\/li\>$/,""); str += "</ul>"; $(document).triggerHandler("context_parse.vakata"); return str.length > 10 ? str : false; }, exec : function (i) { if($.isFunction($.vakata.context.func[i])) { // if is string - eval and call it! $.vakata.context.func[i].call($.vakata.context.data, $.vakata.context.par); return true; } else { return false; } } }; $(function () { var css_string = '' + '#vakata-contextmenu { display:block; visibility:hidden; left:0; top:-200px; position:absolute; margin:0; padding:0; min-width:180px; background:#ebebeb; border:1px solid silver; z-index:10000; *width:180px; } ' + '#vakata-contextmenu ul { min-width:180px; *width:180px; } ' + '#vakata-contextmenu ul, #vakata-contextmenu li { margin:0; padding:0; list-style-type:none; display:block; } ' + '#vakata-contextmenu li { line-height:20px; min-height:20px; position:relative; padding:0px; } ' + '#vakata-contextmenu li a { padding:1px 6px; line-height:17px; display:block; text-decoration:none; margin:1px 1px 0 1px; } ' + '#vakata-contextmenu li ins { float:left; width:16px; height:16px; text-decoration:none; margin-right:2px; } ' + '#vakata-contextmenu li a:hover, #vakata-contextmenu li.vakata-hover > a { background:gray; color:white; } ' + '#vakata-contextmenu li ul { display:none; position:absolute; top:-2px; left:100%; background:#ebebeb; border:1px solid gray; } ' + '#vakata-contextmenu .right { right:100%; left:auto; } ' + '#vakata-contextmenu .bottom { bottom:-1px; top:auto; } ' + '#vakata-contextmenu li.vakata-separator { min-height:0; height:1px; line-height:1px; font-size:1px; overflow:hidden; margin:0 2px; background:silver; /* border-top:1px solid #fefefe; */ padding:0; } '; $.vakata.css.add_sheet({ str : css_string, title : "vakata" }); $.vakata.context.cnt .delegate("a","click", function (e) { e.preventDefault(); }) .delegate("a","mouseup", function (e) { if(!$(this).parent().hasClass("jstree-contextmenu-disabled") && $.vakata.context.exec($(this).attr("rel"))) { $.vakata.context.hide(); } else { $(this).blur(); } }) .delegate("a","mouseover", function () { $.vakata.context.cnt.find(".vakata-hover").removeClass("vakata-hover"); }) .appendTo("body"); $(document).bind("mousedown", function (e) { if($.vakata.context.vis && !$.contains($.vakata.context.cnt[0], e.target)) { $.vakata.context.hide(); } }); if(typeof $.hotkeys !== "undefined") { $(document) .bind("keydown", "up", function (e) { if($.vakata.context.vis) { var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").prevAll("li:not(.vakata-separator)").first(); if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").last(); } o.addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "down", function (e) { if($.vakata.context.vis) { var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").nextAll("li:not(.vakata-separator)").first(); if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").first(); } o.addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "right", function (e) { if($.vakata.context.vis) { $.vakata.context.cnt.find(".vakata-hover").children("ul").show().children("li:not(.vakata-separator)").removeClass("vakata-hover").first().addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "left", function (e) { if($.vakata.context.vis) { $.vakata.context.cnt.find(".vakata-hover").children("ul").hide().children(".vakata-separator").removeClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "esc", function (e) { $.vakata.context.hide(); e.preventDefault(); }) .bind("keydown", "space", function (e) { $.vakata.context.cnt.find(".vakata-hover").last().children("a").click(); e.preventDefault(); }); } }); $.jstree.plugin("contextmenu", { __init : function () { this.get_container() .delegate("a", "contextmenu.jstree", $.proxy(function (e) { e.preventDefault(); if(!$(e.currentTarget).hasClass("jstree-loading")) { this.show_contextmenu(e.currentTarget, e.pageX, e.pageY); } }, this)) .delegate("a", "click.jstree", $.proxy(function (e) { if(this.data.contextmenu) { $.vakata.context.hide(); } }, this)) .bind("destroy.jstree", $.proxy(function () { // TODO: move this to descruct method if(this.data.contextmenu) { $.vakata.context.hide(); } }, this)); $(document).bind("context_hide.vakata", $.proxy(function () { this.data.contextmenu = false; }, this)); }, defaults : { select_node : false, // requires UI plugin show_at_node : true, items : { // Could be a function that should return an object like this one "create" : { "separator_before" : false, "separator_after" : true, "label" : "Create", "action" : function (obj) { this.create(obj); } }, "rename" : { "separator_before" : false, "separator_after" : false, "label" : "Rename", "action" : function (obj) { this.rename(obj); } }, "remove" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Delete", "action" : function (obj) { if(this.is_selected(obj)) { this.remove(); } else { this.remove(obj); } } }, "ccp" : { "separator_before" : true, "icon" : false, "separator_after" : false, "label" : "Edit", "action" : false, "submenu" : { "cut" : { "separator_before" : false, "separator_after" : false, "label" : "Cut", "action" : function (obj) { this.cut(obj); } }, "copy" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Copy", "action" : function (obj) { this.copy(obj); } }, "paste" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Paste", "action" : function (obj) { this.paste(obj); } } } } } }, _fn : { show_contextmenu : function (obj, x, y) { obj = this._get_node(obj); var s = this.get_settings().contextmenu, a = obj.children("a:visible:eq(0)"), o = false, i = false; if(s.select_node && this.data.ui && !this.is_selected(obj)) { this.deselect_all(); this.select_node(obj, true); } if(s.show_at_node || typeof x === "undefined" || typeof y === "undefined") { o = a.offset(); x = o.left; y = o.top + this.data.core.li_height; } i = obj.data("jstree") && obj.data("jstree").contextmenu ? obj.data("jstree").contextmenu : s.items; if($.isFunction(i)) { i = i.call(this, obj); } this.data.contextmenu = true; $.vakata.context.show(i, a, x, y, this, obj, this._get_settings().core.rtl); if(this.data.themes) { $.vakata.context.cnt.attr("class", "jstree-" + this.data.themes.theme + "-context"); } } } }); })(jQuery); //*/ /* * jsTree types plugin * Adds support types of nodes * You can set an attribute on each li node, that represents its type. * According to the type setting the node may get custom icon/validation rules */ (function ($) { $.jstree.plugin("types", { __init : function () { var s = this._get_settings().types; this.data.types.attach_to = []; this.get_container() .bind("init.jstree", $.proxy(function () { var types = s.types, attr = s.type_attr, icons_css = "", _this = this; $.each(types, function (i, tp) { $.each(tp, function (k, v) { if(!/^(max_depth|max_children|icon|valid_children)$/.test(k)) { _this.data.types.attach_to.push(k); } }); if(!tp.icon) { return true; } if( tp.icon.image || tp.icon.position) { if(i == "default") { icons_css += '.jstree-' + _this.get_index() + ' a > .jstree-icon { '; } else { icons_css += '.jstree-' + _this.get_index() + ' li[' + attr + '="' + i + '"] > a > .jstree-icon { '; } if(tp.icon.image) { icons_css += ' background-image:url(' + tp.icon.image + '); '; } if(tp.icon.position){ icons_css += ' background-position:' + tp.icon.position + '; '; } else { icons_css += ' background-position:0 0; '; } icons_css += '} '; } }); if(icons_css !== "") { $.vakata.css.add_sheet({ 'str' : icons_css, title : "jstree-types" }); } }, this)) .bind("before.jstree", $.proxy(function (e, data) { var s, t, o = this._get_settings().types.use_data ? this._get_node(data.args[0]) : false, d = o && o !== -1 && o.length ? o.data("jstree") : false; if(d && d.types && d.types[data.func] === false) { e.stopImmediatePropagation(); return false; } if($.inArray(data.func, this.data.types.attach_to) !== -1) { if(!data.args[0] || (!data.args[0].tagName && !data.args[0].jquery)) { return; } s = this._get_settings().types.types; t = this._get_type(data.args[0]); if( ( (s[t] && typeof s[t][data.func] !== "undefined") || (s["default"] && typeof s["default"][data.func] !== "undefined") ) && this._check(data.func, data.args[0]) === false ) { e.stopImmediatePropagation(); return false; } } }, this)); if(is_ie6) { this.get_container() .bind("load_node.jstree set_type.jstree", $.proxy(function (e, data) { var r = data && data.rslt && data.rslt.obj && data.rslt.obj !== -1 ? this._get_node(data.rslt.obj).parent() : this.get_container_ul(), c = false, s = this._get_settings().types; $.each(s.types, function (i, tp) { if(tp.icon && (tp.icon.image || tp.icon.position)) { c = i === "default" ? r.find("li > a > .jstree-icon") : r.find("li[" + s.type_attr + "='" + i + "'] > a > .jstree-icon"); if(tp.icon.image) { c.css("backgroundImage","url(" + tp.icon.image + ")"); } c.css("backgroundPosition", tp.icon.position || "0 0"); } }); }, this)); } }, defaults : { // defines maximum number of root nodes (-1 means unlimited, -2 means disable max_children checking) max_children : -1, // defines the maximum depth of the tree (-1 means unlimited, -2 means disable max_depth checking) max_depth : -1, // defines valid node types for the root nodes valid_children : "all", // whether to use $.data use_data : false, // where is the type stores (the rel attribute of the LI element) type_attr : "rel", // a list of types types : { // the default type "default" : { "max_children" : -1, "max_depth" : -1, "valid_children": "all" // Bound functions - you can bind any other function here (using boolean or function) //"select_node" : true } } }, _fn : { _types_notify : function (n, data) { if(data.type && this._get_settings().types.use_data) { this.set_type(data.type, n); } }, _get_type : function (obj) { obj = this._get_node(obj); return (!obj || !obj.length) ? false : obj.attr(this._get_settings().types.type_attr) || "default"; }, set_type : function (str, obj) { obj = this._get_node(obj); var ret = (!obj.length || !str) ? false : obj.attr(this._get_settings().types.type_attr, str); if(ret) { this.__callback({ obj : obj, type : str}); } return ret; }, _check : function (rule, obj, opts) { obj = this._get_node(obj); var v = false, t = this._get_type(obj), d = 0, _this = this, s = this._get_settings().types, data = false; if(obj === -1) { if(!!s[rule]) { v = s[rule]; } else { return; } } else { if(t === false) { return; } data = s.use_data ? obj.data("jstree") : false; if(data && data.types && typeof data.types[rule] !== "undefined") { v = data.types[rule]; } else if(!!s.types[t] && typeof s.types[t][rule] !== "undefined") { v = s.types[t][rule]; } else if(!!s.types["default"] && typeof s.types["default"][rule] !== "undefined") { v = s.types["default"][rule]; } } if($.isFunction(v)) { v = v.call(this, obj); } if(rule === "max_depth" && obj !== -1 && opts !== false && s.max_depth !== -2 && v !== 0) { // also include the node itself - otherwise if root node it is not checked obj.children("a:eq(0)").parentsUntil(".jstree","li").each(function (i) { // check if current depth already exceeds global tree depth if(s.max_depth !== -1 && s.max_depth - (i + 1) <= 0) { v = 0; return false; } d = (i === 0) ? v : _this._check(rule, this, false); // check if current node max depth is already matched or exceeded if(d !== -1 && d - (i + 1) <= 0) { v = 0; return false; } // otherwise - set the max depth to the current value minus current depth if(d >= 0 && (d - (i + 1) < v || v < 0) ) { v = d - (i + 1); } // if the global tree depth exists and it minus the nodes calculated so far is less than `v` or `v` is unlimited if(s.max_depth >= 0 && (s.max_depth - (i + 1) < v || v < 0) ) { v = s.max_depth - (i + 1); } }); } return v; }, check_move : function () { if(!this.__call_old()) { return false; } var m = this._get_move(), s = m.rt._get_settings().types, mc = m.rt._check("max_children", m.cr), md = m.rt._check("max_depth", m.cr), vc = m.rt._check("valid_children", m.cr), ch = 0, d = 1, t; if(vc === "none") { return false; } if($.isArray(vc) && m.ot && m.ot._get_type) { m.o.each(function () { if($.inArray(m.ot._get_type(this), vc) === -1) { d = false; return false; } }); if(d === false) { return false; } } if(s.max_children !== -2 && mc !== -1) { ch = m.cr === -1 ? this.get_container().find("> ul > li").not(m.o).length : m.cr.find("> ul > li").not(m.o).length; if(ch + m.o.length > mc) { return false; } } if(s.max_depth !== -2 && md !== -1) { d = 0; if(md === 0) { return false; } if(typeof m.o.d === "undefined") { // TODO: deal with progressive rendering and async when checking max_depth (how to know the depth of the moved node) t = m.o; while(t.length > 0) { t = t.find("> ul > li"); d ++; } m.o.d = d; } if(md - m.o.d < 0) { return false; } } return true; }, create_node : function (obj, position, js, callback, is_loaded, skip_check) { if(!skip_check && (is_loaded || this._is_loaded(obj))) { var p = (typeof position == "string" && position.match(/^before|after$/i) && obj !== -1) ? this._get_parent(obj) : this._get_node(obj), s = this._get_settings().types, mc = this._check("max_children", p), md = this._check("max_depth", p), vc = this._check("valid_children", p), ch; if(typeof js === "string") { js = { data : js }; } if(!js) { js = {}; } if(vc === "none") { return false; } if($.isArray(vc)) { if(!js.attr || !js.attr[s.type_attr]) { if(!js.attr) { js.attr = {}; } js.attr[s.type_attr] = vc[0]; } else { if($.inArray(js.attr[s.type_attr], vc) === -1) { return false; } } } if(s.max_children !== -2 && mc !== -1) { ch = p === -1 ? this.get_container().find("> ul > li").length : p.find("> ul > li").length; if(ch + 1 > mc) { return false; } } if(s.max_depth !== -2 && md !== -1 && (md - 1) < 0) { return false; } } return this.__call_old(true, obj, position, js, callback, is_loaded, skip_check); } } }); })(jQuery); //*/ /* * jsTree HTML plugin * The HTML data store. Datastores are build by replacing the `load_node` and `_is_loaded` functions. */ (function ($) { $.jstree.plugin("html_data", { __init : function () { // this used to use html() and clean the whitespace, but this way any attached data was lost this.data.html_data.original_container_html = this.get_container().find(" > ul > li").clone(true); // remove white space from LI node - otherwise nodes appear a bit to the right this.data.html_data.original_container_html.find("li").andSelf().contents().filter(function() { return this.nodeType == 3; }).remove(); }, defaults : { data : false, ajax : false, correct_state : true }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_html(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { obj = this._get_node(obj); return obj == -1 || !obj || (!this._get_settings().html_data.ajax && !$.isFunction(this._get_settings().html_data.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0; }, load_node_html : function (obj, s_call, e_call) { var d, s = this.get_settings().html_data, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { if(d && d !== "" && d.toString && d.toString().replace(/^[\s\n]+$/,"") !== "") { d = $(d); if(!d.is("ul")) { d = $("<ul />").append(d); } if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } }, this)); break; case (!s.data && !s.ajax): if(!obj || obj == -1) { this.get_container() .children("ul").empty() .append(this.data.html_data.original_container_html) .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end() .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); this.clean_node(); } if(s_call) { s_call.call(this); } break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { d = $(s.data); if(!d.is("ul")) { d = $("<ul />").append(d); } this.get_container() .children("ul").empty().append(d.children()) .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end() .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); this.clean_node(); } if(s_call) { s_call.call(this); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): obj = this._get_node(obj); error_func = function (x, t, e) { var ef = this.get_settings().html_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj != -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { var sf = this.get_settings().html_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) { return error_func.call(this, x, t, ""); } if(d) { d = $(d); if(!d.is("ul")) { d = $("<ul />").append(d); } if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "html"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } } } }); // include the HTML data plugin by default $.jstree.defaults.plugins.push("html_data"); })(jQuery); //*/ /* * jsTree themeroller plugin * Adds support for jQuery UI themes. Include this at the end of your plugins list, also make sure "themes" is not included. */ (function ($) { $.jstree.plugin("themeroller", { __init : function () { var s = this._get_settings().themeroller; this.get_container() .addClass("ui-widget-content") .addClass("jstree-themeroller") .delegate("a","mouseenter.jstree", function (e) { if(!$(e.currentTarget).hasClass("jstree-loading")) { $(this).addClass(s.item_h); } }) .delegate("a","mouseleave.jstree", function () { $(this).removeClass(s.item_h); }) .bind("init.jstree", $.proxy(function (e, data) { data.inst.get_container().find("> ul > li > .jstree-loading > ins").addClass("ui-icon-refresh"); this._themeroller(data.inst.get_container().find("> ul > li")); }, this)) .bind("open_node.jstree create_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.obj); }, this)) .bind("loaded.jstree refresh.jstree", $.proxy(function (e) { this._themeroller(); }, this)) .bind("close_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.obj); }, this)) .bind("delete_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.parent); }, this)) .bind("correct_state.jstree", $.proxy(function (e, data) { data.rslt.obj .children("ins.jstree-icon").removeClass(s.opened + " " + s.closed + " ui-icon").end() .find("> a > ins.ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_open + " " + s.item_clsd).addClass(s.item_leaf || "jstree-no-icon"); }, this)) .bind("select_node.jstree", $.proxy(function (e, data) { data.rslt.obj.children("a").addClass(s.item_a); }, this)) .bind("deselect_node.jstree deselect_all.jstree", $.proxy(function (e, data) { this.get_container() .find("a." + s.item_a).removeClass(s.item_a).end() .find("a.jstree-clicked").addClass(s.item_a); }, this)) .bind("dehover_node.jstree", $.proxy(function (e, data) { data.rslt.obj.children("a").removeClass(s.item_h); }, this)) .bind("hover_node.jstree", $.proxy(function (e, data) { this.get_container() .find("a." + s.item_h).not(data.rslt.obj).removeClass(s.item_h); data.rslt.obj.children("a").addClass(s.item_h); }, this)) .bind("move_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.o); this._themeroller(data.rslt.op); }, this)); }, __destroy : function () { var s = this._get_settings().themeroller, c = [ "ui-icon" ]; $.each(s, function (i, v) { v = v.split(" "); if(v.length) { c = c.concat(v); } }); this.get_container() .removeClass("ui-widget-content") .find("." + c.join(", .")).removeClass(c.join(" ")); }, _fn : { _themeroller : function (obj) { var s = this._get_settings().themeroller; obj = !obj || obj == -1 ? this.get_container_ul() : this._get_node(obj).parent(); obj .find("li.jstree-closed") .children("ins.jstree-icon").removeClass(s.opened).addClass("ui-icon " + s.closed).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_leaf + " " + s.item_open).addClass(s.item_clsd || "jstree-no-icon") .end() .end() .end() .end() .find("li.jstree-open") .children("ins.jstree-icon").removeClass(s.closed).addClass("ui-icon " + s.opened).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_leaf + " " + s.item_clsd).addClass(s.item_open || "jstree-no-icon") .end() .end() .end() .end() .find("li.jstree-leaf") .children("ins.jstree-icon").removeClass(s.closed + " ui-icon " + s.opened).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_clsd + " " + s.item_open).addClass(s.item_leaf || "jstree-no-icon"); } }, defaults : { "opened" : "ui-icon-triangle-1-se", "closed" : "ui-icon-triangle-1-e", "item" : "ui-state-default", "item_h" : "ui-state-hover", "item_a" : "ui-state-active", "item_open" : "ui-icon-folder-open", "item_clsd" : "ui-icon-folder-collapsed", "item_leaf" : "ui-icon-document" } }); $(function() { var css_string = '' + '.jstree-themeroller .ui-icon { overflow:visible; } ' + '.jstree-themeroller a { padding:0 2px; } ' + '.jstree-themeroller .jstree-no-icon { display:none; }'; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree unique plugin * Forces different names amongst siblings (still a bit experimental) * NOTE: does not check language versions (it will not be possible to have nodes with the same title, even in different languages) */ (function ($) { $.jstree.plugin("unique", { __init : function () { this.get_container() .bind("before.jstree", $.proxy(function (e, data) { var nms = [], res = true, p, t; if(data.func == "move_node") { // obj, ref, position, is_copy, is_prepared, skip_check if(data.args[4] === true) { if(data.args[0].o && data.args[0].o.length) { data.args[0].o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); }); res = this._check_unique(nms, data.args[0].np.find("> ul > li").not(data.args[0].o), "move_node"); } } } if(data.func == "create_node") { // obj, position, js, callback, is_loaded if(data.args[4] || this._is_loaded(data.args[0])) { p = this._get_node(data.args[0]); if(data.args[1] && (data.args[1] === "before" || data.args[1] === "after")) { p = this._get_parent(data.args[0]); if(!p || p === -1) { p = this.get_container(); } } if(typeof data.args[2] === "string") { nms.push(data.args[2]); } else if(!data.args[2] || !data.args[2].data) { nms.push(this._get_string("new_node")); } else { nms.push(data.args[2].data); } res = this._check_unique(nms, p.find("> ul > li"), "create_node"); } } if(data.func == "rename_node") { // obj, val nms.push(data.args[1]); t = this._get_node(data.args[0]); p = this._get_parent(t); if(!p || p === -1) { p = this.get_container(); } res = this._check_unique(nms, p.find("> ul > li").not(t), "rename_node"); } if(!res) { e.stopPropagation(); return false; } }, this)); }, defaults : { error_callback : $.noop }, _fn : { _check_unique : function (nms, p, func) { var cnms = []; p.children("a").each(function () { cnms.push($(this).text().replace(/^\s+/g,"")); }); if(!cnms.length || !nms.length) { return true; } cnms = cnms.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); if((cnms.length + nms.length) != cnms.concat(nms).sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(",").length) { this._get_settings().unique.error_callback.call(null, nms, p, func); return false; } return true; }, check_move : function () { if(!this.__call_old()) { return false; } var p = this._get_move(), nms = []; if(p.o && p.o.length) { p.o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); }); return this._check_unique(nms, p.np.find("> ul > li").not(p.o), "check_move"); } return true; } } }); })(jQuery); //*/ /* * jsTree wholerow plugin * Makes select and hover work on the entire width of the node * MAY BE HEAVY IN LARGE DOM */ (function ($) { $.jstree.plugin("wholerow", { __init : function () { if(!this.data.ui) { throw "jsTree wholerow: jsTree UI plugin not included."; } this.data.wholerow.html = false; this.data.wholerow.to = false; this.get_container() .bind("init.jstree", $.proxy(function (e, data) { this._get_settings().core.animation = 0; }, this)) .bind("open_node.jstree create_node.jstree clean_node.jstree loaded.jstree", $.proxy(function (e, data) { this._prepare_wholerow_span( data && data.rslt && data.rslt.obj ? data.rslt.obj : -1 ); }, this)) .bind("search.jstree clear_search.jstree reopen.jstree after_open.jstree after_close.jstree create_node.jstree delete_node.jstree clean_node.jstree", $.proxy(function (e, data) { if(this.data.to) { clearTimeout(this.data.to); } this.data.to = setTimeout( (function (t, o) { return function() { t._prepare_wholerow_ul(o); }; })(this, data && data.rslt && data.rslt.obj ? data.rslt.obj : -1), 0); }, this)) .bind("deselect_all.jstree", $.proxy(function (e, data) { this.get_container().find(" > .jstree-wholerow .jstree-clicked").removeClass("jstree-clicked " + (this.data.themeroller ? this._get_settings().themeroller.item_a : "" )); }, this)) .bind("select_node.jstree deselect_node.jstree ", $.proxy(function (e, data) { data.rslt.obj.each(function () { var ref = data.inst.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt((($(this).offset().top - data.inst.get_container().offset().top + data.inst.get_container()[0].scrollTop) / data.inst.data.core.li_height),10)) + ")"); // ref.children("a")[e.type === "select_node" ? "addClass" : "removeClass"]("jstree-clicked"); ref.children("a").attr("class",data.rslt.obj.children("a").attr("class")); }); }, this)) .bind("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) { this.get_container().find(" > .jstree-wholerow .jstree-hovered").removeClass("jstree-hovered " + (this.data.themeroller ? this._get_settings().themeroller.item_h : "" )); if(e.type === "hover_node") { var ref = this.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt(((data.rslt.obj.offset().top - this.get_container().offset().top + this.get_container()[0].scrollTop) / this.data.core.li_height),10)) + ")"); // ref.children("a").addClass("jstree-hovered"); ref.children("a").attr("class",data.rslt.obj.children(".jstree-hovered").attr("class")); } }, this)) .delegate(".jstree-wholerow-span, ins.jstree-icon, li", "click.jstree", function (e) { var n = $(e.currentTarget); if(e.target.tagName === "A" || (e.target.tagName === "INS" && n.closest("li").is(".jstree-open, .jstree-closed"))) { return; } n.closest("li").children("a:visible:eq(0)").click(); e.stopImmediatePropagation(); }) .delegate("li", "mouseover.jstree", $.proxy(function (e) { e.stopImmediatePropagation(); if($(e.currentTarget).children(".jstree-hovered, .jstree-clicked").length) { return false; } this.hover_node(e.currentTarget); return false; }, this)) .delegate("li", "mouseleave.jstree", $.proxy(function (e) { if($(e.currentTarget).children("a").hasClass("jstree-hovered").length) { return; } this.dehover_node(e.currentTarget); }, this)); if(is_ie7 || is_ie6) { $.vakata.css.add_sheet({ str : ".jstree-" + this.get_index() + " { position:relative; } ", title : "jstree" }); } }, defaults : { }, __destroy : function () { this.get_container().children(".jstree-wholerow").remove(); this.get_container().find(".jstree-wholerow-span").remove(); }, _fn : { _prepare_wholerow_span : function (obj) { obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj); if(obj === false) { return; } // added for removing root nodes obj.each(function () { $(this).find("li").andSelf().each(function () { var $t = $(this); if($t.children(".jstree-wholerow-span").length) { return true; } $t.prepend("<span class='jstree-wholerow-span' style='width:" + ($t.parentsUntil(".jstree","li").length * 18) + "px;'> </span>"); }); }); }, _prepare_wholerow_ul : function () { var o = this.get_container().children("ul").eq(0), h = o.html(); o.addClass("jstree-wholerow-real"); if(this.data.wholerow.last_html !== h) { this.data.wholerow.last_html = h; this.get_container().children(".jstree-wholerow").remove(); this.get_container().append( o.clone().removeClass("jstree-wholerow-real") .wrapAll("<div class='jstree-wholerow' />").parent() .width(o.parent()[0].scrollWidth) .css("top", (o.height() + ( is_ie7 ? 5 : 0)) * -1 ) .find("li[id]").each(function () { this.removeAttribute("id"); }).end() ); } } } }); $(function() { var css_string = '' + '.jstree .jstree-wholerow-real { position:relative; z-index:1; } ' + '.jstree .jstree-wholerow-real li { cursor:pointer; } ' + '.jstree .jstree-wholerow-real a { border-left-color:transparent !important; border-right-color:transparent !important; } ' + '.jstree .jstree-wholerow { position:relative; z-index:0; height:0; } ' + '.jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { width:100%; } ' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li, .jstree .jstree-wholerow a { margin:0 !important; padding:0 !important; } ' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { background:transparent !important; }' + '.jstree .jstree-wholerow ins, .jstree .jstree-wholerow span, .jstree .jstree-wholerow input { display:none !important; }' + '.jstree .jstree-wholerow a, .jstree .jstree-wholerow a:hover { text-indent:-9999px; !important; width:100%; padding:0 !important; border-right-width:0px !important; border-left-width:0px !important; } ' + '.jstree .jstree-wholerow-span { position:absolute; left:0; margin:0px; padding:0; height:18px; border-width:0; padding:0; z-index:0; }'; if(is_ff2) { css_string += '' + '.jstree .jstree-wholerow a { display:block; height:18px; margin:0; padding:0; border:0; } ' + '.jstree .jstree-wholerow-real a { border-color:transparent !important; } '; } if(is_ie7 || is_ie6) { css_string += '' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow li, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow a { margin:0; padding:0; line-height:18px; } ' + '.jstree .jstree-wholerow a { display:block; height:18px; line-height:18px; overflow:hidden; } '; } $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree model plugin * This plugin gets jstree to use a class model to retrieve data, creating great dynamism */ (function ($) { var nodeInterface = ["getChildren","getChildrenCount","getAttr","getName","getProps"], validateInterface = function(obj, inter) { var valid = true; obj = obj || {}; inter = [].concat(inter); $.each(inter, function (i, v) { if(!$.isFunction(obj[v])) { valid = false; return false; } }); return valid; }; $.jstree.plugin("model", { __init : function () { if(!this.data.json_data) { throw "jsTree model: jsTree json_data plugin not included."; } this._get_settings().json_data.data = function (n, b) { var obj = (n == -1) ? this._get_settings().model.object : n.data("jstree_model"); if(!validateInterface(obj, nodeInterface)) { return b.call(null, false); } if(this._get_settings().model.async) { obj.getChildren($.proxy(function (data) { this.model_done(data, b); }, this)); } else { this.model_done(obj.getChildren(), b); } }; }, defaults : { object : false, id_prefix : false, async : false }, _fn : { model_done : function (data, callback) { var ret = [], s = this._get_settings(), _this = this; if(!$.isArray(data)) { data = [data]; } $.each(data, function (i, nd) { var r = nd.getProps() || {}; r.attr = nd.getAttr() || {}; if(nd.getChildrenCount()) { r.state = "closed"; } r.data = nd.getName(); if(!$.isArray(r.data)) { r.data = [r.data]; } if(_this.data.types && $.isFunction(nd.getType)) { r.attr[s.types.type_attr] = nd.getType(); } if(r.attr.id && s.model.id_prefix) { r.attr.id = s.model.id_prefix + r.attr.id; } if(!r.metadata) { r.metadata = { }; } r.metadata.jstree_model = nd; ret.push(r); }); callback.call(null, ret); } } }); })(jQuery); //*/ })(); define("thirdparty/jstree_pre1.0_fix_1/jquery.jstree", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * PreferenceStorage defines an interface for persisting preference data as * name/value pairs for a module or plugin. */ define('preferences/PreferenceStorage',['require','exports','module','preferences/PreferencesManager'],function (require, exports, module) { var PreferencesManager = require("preferences/PreferencesManager"); /** * @private * Validate JSON keys and values. */ function _validateJSONPair(key, value) { if (typeof key === "string") { // validate temporary JSON var temp = {}, error = null; temp[key] = value; try { temp = JSON.parse(JSON.stringify(temp)); } catch (err) { error = err; } // set value to JSON storage if no errors occurred if (!error && (temp[key] !== undefined)) { return true; } else { throw new Error("Value '" + value + "' for key '" + key + "' must be a valid JSON value"); } } else { throw new Error("Preference key '" + key + "' must be a string"); } } /** * @private * Save to persistent storage. */ function _commit() { PreferencesManager.savePreferences(); } /** * Creates a new PreferenceStorage object. * @param {!string} clientID Unique identifier for PreferencesManager to * associate this PreferenceStorage data with. * @param {!object} json JSON object to persist preference data. */ function PreferenceStorage(clientID, json) { this._clientID = clientID; this._json = json; } /** * Unique clientID for this PreferenceStorage object. * @return {!string} clientID */ PreferenceStorage.prototype.getClientID = function () { return this._clientID; }; /** * Removes a preference from this PreferenceStorage object. * @param {!string} key A unique identifier */ PreferenceStorage.prototype.remove = function (key) { // remove value from JSON storage delete this._json[key]; _commit(); }; /** * Assigns a value for a key. Overwrites existing value if present. * @param {!string} key A unique identifier * @param {object} value A valid JSON value */ PreferenceStorage.prototype.setValue = function (key, value) { if (_validateJSONPair(key, value)) { this._json[key] = value; _commit(); } }; /** * Retreive the value associated with the specified key. * @param {!string} key Key name to lookup. * @return {object} Returns the value for the key or undefined. */ PreferenceStorage.prototype.getValue = function (key) { return this._json[key]; }; /** * Return all name-value pairs as a single JSON object. * @return {!object} JSON object containing name/value pairs for all keys * in this PreferenceStorage object. */ PreferenceStorage.prototype.getAllValues = function () { return JSON.parse(JSON.stringify(this._json)); }; /** * Writes name-value pairs from a JSON object as preference properties. * Invalid JSON values throw an error and all changes are discarded. * * @param {!object} obj A JSON object with zero or more preference properties to write. * @param {boolean} append Defaults to false. When true, properties in the JSON object * overwrite and/or append to the existing set of preference properties. When false, * all existing preferences are deleted before writing new properties from the JSON object. */ PreferenceStorage.prototype.setAllValues = function (obj, append) { var self = this, error = null; // validate all name/value pairs before committing $.each(obj, function (key, value) { try { _validateJSONPair(key, value); } catch (err) { // fail fast error = err; return false; } }); // skip changes if any error is detected if (error) { throw error; } // delete all exiting properties if not appending if (!append) { $.each(this._json, function (key, value) { delete self._json[key]; }); } // copy properties from incoming JSON object $.each(obj, function (key, value) { self._json[key] = value; }); _commit(); }; exports.PreferenceStorage = PreferenceStorage; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, localStorage */ /** * PreferencesManager * */ define('preferences/PreferencesManager',['require','exports','module','preferences/PreferenceStorage'],function (require, exports, module) { var PreferenceStorage = require("preferences/PreferenceStorage").PreferenceStorage; var PREFERENCES_KEY = "com.adobe.brackets.preferences"; // Private Properties var preferencesKey, prefStorage, persistentStorage, doLoadPreferences = false; /** * Retreive preferences data for the given clientID. * * @param {string} clientID Unique identifier * @param {string} defaults Default preferences stored as JSON * @return {PreferenceStorage} */ function getPreferenceStorage(clientID, defaults) { if ((clientID === undefined) || (clientID === null)) { throw new Error("Invalid clientID"); } var prefs = prefStorage[clientID]; if (prefs === undefined) { // create a new empty preferences object prefs = (defaults && JSON.stringify(defaults)) ? defaults : {}; prefStorage[clientID] = prefs; } return new PreferenceStorage(clientID, prefs); } /** * Save all preference clients. */ function savePreferences() { // save all preferences persistentStorage.setItem(preferencesKey, JSON.stringify(prefStorage)); } /** * @private * Reset preferences and callbacks */ function _reset() { prefStorage = {}; // Note that storage.clear() is not used. Production and unit test code // both rely on the same backing storage but unique item keys. persistentStorage.setItem(preferencesKey, JSON.stringify(prefStorage)); } /** * @private * Initialize persistent storage implementation */ function _initStorage(storage) { persistentStorage = storage; if (doLoadPreferences) { prefStorage = JSON.parse(persistentStorage.getItem(preferencesKey)); } // initialize empty preferences if none were found in storage if (!prefStorage) { _reset(); } } // Check localStorage for a preferencesKey. Production and unit test keys // are used to keep preferences separate within the same storage implementation. preferencesKey = localStorage.getItem("preferencesKey"); if (!preferencesKey) { // use default key if none is found preferencesKey = PREFERENCES_KEY; doLoadPreferences = true; } else { // using a non-default key, check for additional settings doLoadPreferences = !!(localStorage.getItem("doLoadPreferences")); } // Use localStorage by default _initStorage(localStorage); // Public API exports.getPreferenceStorage = getPreferenceStorage; exports.savePreferences = savePreferences; // Unit test use only exports._reset = _reset; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Manages global application commands that can be called from menu items, key bindings, or subparts * of the application. */ define('command/CommandManager',['require','exports','module'],function (require, exports, module) { /** * Map of all registered global commands * @type Object.<commandID: string, Command> */ var _commands = {}; /** * Temporary copy of commands map for restoring after testing * TODO (issue #1039): implement separate require contexts for unit tests * @type Object.<commandID: string, Command> */ var _commandsOriginal = {}; /** * @constructor * @private * * @param {string} name - text that will be displayed in the UI to represent command * @param {string} id * @param {function} commandFn - the function that is called when the command is executed. * * TODO: where should this be triggered, The Command or Exports? * Events: * enabledStateChange * checkedStateChange * keyBindingAdded * keyBindingRemoved */ function Command(name, id, commandFn) { this._name = name; this._id = id; this._commandFn = commandFn; this._checked = undefined; this._enabled = true; } /** @return {Command} */ Command.prototype.getID = function () { return this._id; }; /** * Executes the command. Additional arguments are passed to the executing function * * @return {$.Promise} a jQuery promise that will be resolved when the command completes. */ Command.prototype.execute = function () { if (!this._enabled) { return; } var result = this._commandFn.apply(this, arguments); if (!result) { return (new $.Deferred()).resolve().promise(); } else { return result; } }; /** @return {boolean} */ Command.prototype.getEnabled = function () { return this._enabled; }; /** * Sets enabled state of Command and dispatches "enabledStateChange" * when the enabled state changes. * @param {boolean} enabled */ Command.prototype.setEnabled = function (enabled) { var changed = this._enabled !== enabled; this._enabled = enabled; if (changed) { $(this).triggerHandler("enabledStateChange"); } }; /** * Sets enabled state of Command and dispatches "checkedStateChange" * when the enabled state changes. * @param {boolean} checked */ Command.prototype.setChecked = function (checked) { var changed = this._checked !== checked; this._checked = checked; if (changed) { $(this).triggerHandler("checkedStateChange"); } }; /** @return {boolean} */ Command.prototype.getChecked = function () { return this._checked; }; /** * Sets the name of the Command and dispatches "nameChange" so that * UI that reflects the command name can update. * * Note, a Command name can appear in either HTML or native UI * so HTML tags should not be used. To add a Unicode character, * use \uXXXX instead of an HTML entity. * * @param {string} name */ Command.prototype.setName = function (name) { var changed = this._name !== name; this._name = name; if (changed) { $(this).triggerHandler("nameChange"); } }; /** @return {string} */ Command.prototype.getName = function () { return this._name; }; /** * Registers a global command. * @param {string} name - text that will be displayed in the UI to represent command * @param {string} id - unique identifier for command. * Core commands in Brackets use a simple command title as an id, for example "open.file". * Extensions should use the following format: "author.myextension.mycommandname". * For example, "lschmitt.csswizard.format.css". * @param {function(...)} commandFn - the function to call when the command is executed. Any arguments passed to * execute() (after the id) are passed as arguments to the function. If the function is asynchronous, * it must return a jQuery promise that is resolved when the command completes. Otherwise, the * CommandManager will assume it is synchronous, and return a promise that is already resolved. * @return {?Command} */ function register(name, id, commandFn) { if (_commands[id]) { console.log("Attempting to register an already-registered command: " + id); return null; } if (!name || !id || !commandFn) { throw new Error("Attempting to register a command with a missing name, id, or command function:" + name + " " + id); } var command = new Command(name, id, commandFn); _commands[id] = command; return command; } /** * Clear all commands for unit testing, but first make copy of commands so that * they can be restored afterward */ function _testReset() { _commandsOriginal = _commands; _commands = {}; } /** * Restore original commands after test and release copy */ function _testRestore() { _commands = _commandsOriginal; _commandsOriginal = {}; } /** * Retrieves a Command object by id * @param {string} id * @return {Command} */ function get(id) { return _commands[id]; } /** * Returns the ids of all registered commands * @return {Array.<string>} */ function getAll() { return Object.keys(_commands); } /** * Looks up and runs a global command. Additional arguments are passed to the command. * * @param {string} id The ID of the command to run. * @return {$.Promise} a jQuery promise that will be resolved when the command completes. */ function execute(id) { var command = _commands[id]; if (command) { return command.execute.apply(command, Array.prototype.slice.call(arguments, 1)); } else { return (new $.Deferred()).reject().promise(); } } // Define public API exports.register = register; exports.execute = execute; exports.get = get; exports.getAll = getAll; exports._testReset = _testReset; exports._testRestore = _testRestore; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('command/Commands',['require','exports','module'],function (require, exports, module) { /** * List of constants for global command IDs. */ // FILE exports.FILE_NEW = "file.new"; exports.FILE_OPEN = "file.open"; exports.FILE_OPEN_FOLDER = "file.openFolder"; exports.FILE_SAVE = "file.save"; exports.FILE_SAVE_ALL = "file.saveAll"; exports.FILE_CLOSE = "file.close"; exports.FILE_CLOSE_ALL = "file.close_all"; exports.FILE_CLOSE_WINDOW = "file.close_window"; // string must MATCH string in native code (brackets_extensions) exports.FILE_ADD_TO_WORKING_SET = "file.addToWorkingSet"; exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview"; exports.FILE_QUIT = "file.quit"; // string must MATCH string in native code (brackets_extensions) // EDIT exports.EDIT_UNDO = "edit.undo"; exports.EDIT_REDO = "edit.redo"; exports.EDIT_CUT = "edit.cut"; exports.EDIT_COPY = "edit.copy"; exports.EDIT_PASTE = "edit.paste"; exports.EDIT_SELECT_ALL = "edit.selectAll"; exports.EDIT_FIND = "edit.find"; exports.EDIT_FIND_IN_FILES = "edit.findInFiles"; exports.EDIT_FIND_NEXT = "edit.findNext"; exports.EDIT_FIND_PREVIOUS = "edit.findPrevious"; exports.EDIT_REPLACE = "edit.replace"; exports.EDIT_INDENT = "edit.indent"; exports.EDIT_UNINDENT = "edit.unindent"; exports.EDIT_DUPLICATE = "edit.duplicate"; exports.EDIT_LINE_COMMENT = "edit.lineComment"; exports.EDIT_LINE_UP = "edit.lineUp"; exports.EDIT_LINE_DOWN = "edit.lineDown"; exports.TOGGLE_USE_TAB_CHARS = "debug.useTabChars"; // VIEW exports.VIEW_HIDE_SIDEBAR = "view.toggleSidebar"; exports.VIEW_INCREASE_FONT_SIZE = "view.increaseFontSize"; exports.VIEW_DECREASE_FONT_SIZE = "view.decreaseFontSize"; exports.VIEW_RESTORE_FONT_SIZE = "view.restoreFontSize"; exports.TOGGLE_JSLINT = "debug.jslint"; // Navigate exports.NAVIGATE_NEXT_DOC = "navigate.nextDoc"; exports.NAVIGATE_PREV_DOC = "navigate.prevDoc"; exports.NAVIGATE_QUICK_OPEN = "navigate.quickOpen"; exports.NAVIGATE_GOTO_DEFINITION = "navigate.gotoDefinition"; exports.NAVIGATE_GOTO_LINE = "navigate.gotoLine"; exports.TOGGLE_QUICK_EDIT = "navigate.toggleQuickEdit"; exports.QUICK_EDIT_NEXT_MATCH = "navigate.nextMatch"; exports.QUICK_EDIT_PREV_MATCH = "navigate.previousMatch"; // Debug exports.DEBUG_REFRESH_WINDOW = "debug.refreshWindow"; // string must MATCH string in native code (brackets_extensions) exports.DEBUG_SHOW_DEVELOPER_TOOLS = "debug.showDeveloperTools"; exports.DEBUG_RUN_UNIT_TESTS = "debug.runUnitTests"; exports.DEBUG_SHOW_PERF_DATA = "debug.showPerfData"; exports.DEBUG_NEW_BRACKETS_WINDOW = "debug.newBracketsWindow"; exports.DEBUG_SHOW_EXT_FOLDER = "debug.showExtensionsFolder"; exports.DEBUG_SWITCH_LANGUAGE = "debug.switchLanguage"; exports.CHECK_FOR_UPDATE = "app.checkForUpdate"; // Command that does nothing. Can be used for place holder menuItems exports.HELP_ABOUT = "help.about"; // File shell callbacks exports.APP_ABORT_QUIT = "app.abort_quit"; // string must MATCH string in native code (appshell_extensions) }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */ /*global define, $, brackets, window */ /** * Manages the mapping of keyboard inputs to commands. */ define('command/KeyBindingManager',['require','exports','module','command/CommandManager'],function (require, exports, module) { var CommandManager = require("command/CommandManager"); /** * Maps normalized shortcut descriptor to key binding info. * @type {!Object.<string, {commandID: string, key: string, displayKey: string}>} */ var _keyMap = {}; /** * Maps commandID to the list of shortcuts that are bound to it. * @type {!Object.<string, Array.<{key: string, displayKey: string}>>} */ var _commandMap = {}; /** * Allow clients to toggle key binding */ var _enabled = true; /** * @private */ function _reset() { _keyMap = {}; _commandMap = {}; } /** * @private * Initialize an empty keymap as the current keymap. It overwrites the current keymap if there is one. * builds the keyDescriptor string from the given parts * @param {boolean} hasCtrl Is Ctrl key enabled * @param {boolean} hasAlt Is Alt key enabled * @param {boolean} hasShift Is Shift key enabled * @param {string} key The key that's pressed * @return {string} The normalized key descriptor */ function _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key) { if (!key) { console.log("KeyBindingManager _buildKeyDescriptor() - No key provided!"); return ""; } var keyDescriptor = []; if (hasMacCtrl) { keyDescriptor.push("Ctrl"); } if (hasAlt) { keyDescriptor.push("Alt"); } if (hasShift) { keyDescriptor.push("Shift"); } if (hasCtrl) { // Windows display Ctrl first, Mac displays Command symbol last if (brackets.platform === "mac") { keyDescriptor.push("Cmd"); } else { keyDescriptor.unshift("Ctrl"); } } keyDescriptor.push(key); return keyDescriptor.join("-"); } /** * normalizes the incoming key descriptor so the modifier keys are always specified in the correct order * @param {string} The string for a key descriptor, can be in any order, the result will be Ctrl-Alt-Shift-<Key> * @return {string} The normalized key descriptor or null if the descriptor invalid */ function normalizeKeyDescriptorString(origDescriptor) { var hasMacCtrl = false, hasCtrl = false, hasAlt = false, hasShift = false, key = "", error = false; function _compareModifierString(left, right, previouslyFound, origDescriptor) { if (!left || !right) { return false; } left = left.trim().toLowerCase(); right = right.trim().toLowerCase(); var matched = (left.length > 0 && left === right); if (matched && previouslyFound) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Modifier defined twice: " + origDescriptor); } return matched; } origDescriptor.split("-").forEach(function parseDescriptor(ele, i, arr) { if (_compareModifierString("ctrl", ele, hasCtrl)) { if (brackets.platform === "mac") { hasMacCtrl = true; } else { hasCtrl = true; } } else if (_compareModifierString("cmd", ele, hasCtrl, origDescriptor)) { hasCtrl = true; } else if (_compareModifierString("alt", ele, hasAlt, origDescriptor)) { hasAlt = true; } else if (_compareModifierString("opt", ele, hasAlt, origDescriptor)) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Opt getting mapped to Alt from: " + origDescriptor); hasAlt = true; } else if (_compareModifierString("shift", ele, hasShift, origDescriptor)) { hasShift = true; } else if (key.length > 0) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Multiple keys defined. Using key: " + key + " from: " + origDescriptor); error = true; } else { key = ele; } }); if (error) { return null; } // Check to see if the binding is for "-". if (key === "" && origDescriptor.search(/^.+--$/) !== -1) { key = "-"; } // '+' char is valid if it's the only key. Keyboard shortcut strings should use // unicode characters (unescaped). Keyboard shortcut display strings may use // unicode escape sequences (e.g. \u20AC euro sign) if ((key.indexOf("+")) >= 0 && (key.length > 1)) { return null; } return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key); } /** * @private * Looks for keycodes that have os-inconsistent keys and fixes them. * @param {number} The keycode from the keyboard event. * @param {string} The current best guess at what the key is. * @return {string} If the key is OS-inconsistent, the correct key; otherwise, the original key. **/ function _mapKeycodeToKey(keycode, key) { switch (keycode) { case 186: return ";"; case 187: return "="; case 188: return ","; case 189: return "-"; case 190: return "."; case 191: return "/"; case 192: return "`"; case 219: return "["; case 220: return "\\"; case 221: return "]"; case 222: return "'"; default: return key; } } /** * Takes a keyboard event and translates it into a key in a key map */ function _translateKeyboardEvent(event) { var hasMacCtrl = (brackets.platform === "win") ? false : (event.ctrlKey), hasCtrl = (brackets.platform === "win") ? (event.ctrlKey) : (event.metaKey), hasAlt = (event.altKey), hasShift = (event.shiftKey), key = String.fromCharCode(event.keyCode); //From the W3C, if we can get the KeyboardEvent.keyIdentifier then look here //As that will let us use keys like then function keys "F5" for commands. The //full set of values we can use is here //http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set var ident = event.keyIdentifier; if (ident) { if (ident.charAt(0) === "U" && ident.charAt(1) === "+") { //This is a unicode code point like "U+002A", get the 002A and use that key = String.fromCharCode(parseInt(ident.substring(2), 16)); } else { //This is some non-character key, just use the raw identifier key = ident; } } // Translate some keys to their common names if (key === "\t") { key = "Tab"; } else if (key === " ") { key = "Space"; } else { key = _mapKeycodeToKey(event.keyCode, key); } return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key); } /** * Convert normalized key representation to display appropriate for platform. * @param {!string} descriptor Normalized key descriptor. * @return {!string} Display/Operating system appropriate string */ function formatKeyDescriptor(descriptor) { var displayStr; if (brackets.platform === "mac") { displayStr = descriptor.replace(/-/g, ""); // remove dashes displayStr = displayStr.replace("Ctrl", "\u2303"); // Ctrl > control symbol displayStr = displayStr.replace("Cmd", "\u2318"); // Cmd > command symbol displayStr = displayStr.replace("Shift", "\u21E7"); // Shift > shift symbol displayStr = displayStr.replace("Alt", "\u2325"); // Alt > option symbol } else { displayStr = descriptor.replace(/-/g, "+"); } return displayStr; } /** * @private * @param {string} A normalized key-description string. * @return {boolean} true if the key is already assigned, false otherwise. */ function _isKeyAssigned(key) { return (_keyMap[key] !== undefined); } /** * @private * * @param {string} commandID * @param {string|{{key: string, displayKey: string}}} keyBinding - a single shortcut. * @param {?string} platform - undefined indicates all platforms * @return {?{key: string, displayKey:String}} Returns a record for valid key bindings */ function _addBinding(commandID, keyBinding, platform) { var key, result = null, normalized, normalizedDisplay, explicitPlatform = keyBinding.platform || platform, targetPlatform = explicitPlatform || brackets.platform, command; // skip if this binding doesn't match the current platform if (targetPlatform !== brackets.platform) { return null; } key = (keyBinding.key) || keyBinding; if (brackets.platform === "mac" && explicitPlatform === undefined) { key = key.replace("Ctrl", "Cmd"); if (keyBinding.displayKey !== undefined) { keyBinding.displayKey = keyBinding.displayKey.replace("Ctrl", "Cmd"); } } normalized = normalizeKeyDescriptorString(key); // skip if the key binding is invalid if (!normalized) { console.log("Failed to normalize " + key); return null; } // skip if the key is already assigned if (_isKeyAssigned(normalized)) { console.log("Cannot assign " + normalized + " to " + commandID + ". It is already assigned to " + _keyMap[normalized]); return null; } // optional display-friendly string (e.g. CMD-+ instead of CMD-=) normalizedDisplay = (keyBinding.displayKey) ? normalizeKeyDescriptorString(keyBinding.displayKey) : normalized; // 1-to-many commandID mapping to key binding if (!_commandMap[commandID]) { _commandMap[commandID] = []; } result = {key: normalized, displayKey: normalizedDisplay}; _commandMap[commandID].push(result); // 1-to-1 key binding to commandID _keyMap[normalized] = {commandID: commandID, key: normalized, displayKey: normalizedDisplay}; // notify listeners command = CommandManager.get(commandID); if (command) { $(command).triggerHandler("keyBindingAdded", [result]); } return result; } /** * Returns a copy of the keymap * @returns {!Object.<string, {commandID: string, key: string, displayKey: string}>} */ function getKeymap() { return $.extend({}, _keyMap); } /** * Process the keybinding for the current key. * * @param {string} A key-description string. * @return {boolean} true if the key was processed, false otherwise */ function handleKey(key) { if (_enabled && _keyMap[key]) { CommandManager.execute(_keyMap[key].commandID); return true; } return false; } // TODO (issue #414): Replace this temporary fix with a more robust solution to handle focus and modality /** * Enable or disable key bindings. Clients such as dialogs may wish to disable * global key bindings temporarily. * * @param {string} A key-description string. * @return {boolean} true if the key was processed, false otherwise */ function setEnabled(value) { _enabled = value; } /** * Add one or more key bindings to a particular Command. * * @param {!string} commandID * @param {?({key: string, displayKey: string} | Array.<{key: string, displayKey: string, platform: string)}>} keyBindings - a single key binding * or an array of keybindings. Example: "Shift-Cmd-F". Mac and Win key equivalents are automatically * mapped to each other. Use displayKey property to display a different string (e.g. "CMD+" instead of "CMD="). * @param {?string} platform - the target OS of the keyBindings either "mac" or "win". If undefined, all platforms will use * the key binding. Ignored if keyBindings is passed an Array. * @return {{key: string, displayKey:String}|Array.<{key: string, displayKey:String}>} Returns record(s) for valid key binding(s) */ function addBinding(commandID, keyBindings, platform) { if ((commandID === null) || (commandID === undefined) || !keyBindings) { return; } var normalizedBindings = [], targetPlatform, results; if (Array.isArray(keyBindings)) { var keyBinding; results = []; keyBindings.forEach(function (keyBindingRequest) { keyBinding = _addBinding(commandID, keyBindingRequest, keyBindingRequest.platform); if (keyBinding) { results.push(keyBinding); } }); } else { results = _addBinding(commandID, keyBindings, platform); } return results; } /** * Remove a key binding from _keymap * * @param {!string} key - a key-description string that may or may not be normalized. * @param {?string} platform - OS from which to remove the binding (all platforms if unspecified) */ function removeBinding(key, platform) { if (!key || ((platform !== null) && (platform !== undefined) && (platform !== brackets.platform))) { return; } var normalizedKey = normalizeKeyDescriptorString(key); if (!normalizedKey) { console.log("Fail to nomalize " + key); } else if (_isKeyAssigned(normalizedKey)) { var binding = _keyMap[normalizedKey], command = CommandManager.get(binding.commandID), bindings = _commandMap[binding.commandID]; // delete key binding record delete _keyMap[normalizedKey]; if (bindings) { // delete mapping from command to key binding _commandMap[binding.commandID] = bindings.filter(function (b) { return (b.key !== normalizedKey); }); if (command) { $(command).triggerHandler("keyBindingRemoved", [{key: normalizedKey, displayKey: binding.displayKey}]); } } } } /** * Retrieve key bindings currently associated with a command * * @param {!string} command - A command ID * @return {!Array.<{{key: string, displayKey: string}}>} An array of associated key bindings. */ function getKeyBindings(commandID) { var bindings = _commandMap[commandID]; return bindings || []; } /** * Install keydown event listener. */ function init() { // init window.document.body.addEventListener( "keydown", function (event) { if (handleKey(_translateKeyboardEvent(event))) { event.stopPropagation(); } }, true ); } // unit test only exports._reset = _reset; // Define public API exports.init = init; exports.getKeymap = getKeymap; exports.handleKey = handleKey; exports.setEnabled = setEnabled; exports.addBinding = addBinding; exports.removeBinding = removeBinding; exports.formatKeyDescriptor = formatKeyDescriptor; exports.getKeyBindings = getKeyBindings; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window */ /** * Utilities for creating and managing standard modal dialogs. */ define('widgets/Dialogs',['require','exports','module','command/KeyBindingManager'],function (require, exports, module) { var KeyBindingManager = require("command/KeyBindingManager"); var DIALOG_BTN_CANCEL = "cancel", DIALOG_BTN_OK = "ok", DIALOG_BTN_DONTSAVE = "dontsave", DIALOG_CANCELED = "_canceled", DIALOG_BTN_DOWNLOAD = "download"; // TODO: (issue #258) In future, we should templatize the HTML for the dialogs rather than having // it live directly in the HTML. var DIALOG_ID_ERROR = "error-dialog", DIALOG_ID_SAVE_CLOSE = "save-close-dialog", DIALOG_ID_EXT_CHANGED = "ext-changed-dialog", DIALOG_ID_EXT_DELETED = "ext-deleted-dialog", DIALOG_ID_LIVE_DEVELOPMENT = "live-development-error-dialog", DIALOG_ID_ABOUT = "about-dialog", DIALOG_ID_UPDATE = "update-dialog"; function _dismissDialog(dlg, buttonId) { dlg.data("buttonId", buttonId); dlg.modal(true).hide(); } function _hasButton(dlg, buttonId) { return dlg.find("[data-button-id='" + buttonId + "']"); } var _handleKeyDown = function (e) { var primaryBtn = this.find(".primary"), buttonId = null, which = String.fromCharCode(e.which); if (e.which === 13) { // Click primary button if (primaryBtn) { buttonId = primaryBtn.attr("data-button-id"); } } else if (e.which === 32) { // Space bar on focused button this.find(".dialog-button:focus").click(); } else if (brackets.platform === "mac") { // CMD+D Don't Save if (e.metaKey && (which === "D")) { if (_hasButton(this, DIALOG_BTN_DONTSAVE)) { buttonId = DIALOG_BTN_DONTSAVE; } // FIXME (issue #418) CMD+. Cancel swallowed by native shell } else if (e.metaKey && (e.which === 190)) { buttonId = DIALOG_BTN_CANCEL; } } else { // if (brackets.platform === "win") { // 'N' Don't Save if (which === "N") { if (_hasButton(this, DIALOG_BTN_DONTSAVE)) { buttonId = DIALOG_BTN_DONTSAVE; } } } if (buttonId) { _dismissDialog(this, buttonId); } else if (!($.contains(this.get(0), e.target)) || ($(e.target).filter(":input").length === 0)) { // Stop the event if the target is not inside the dialog // or if the target is not a form element. // TODO (issue #414): more robust handling of dialog scoped // vs. global key bindings e.stopPropagation(); e.preventDefault(); } }; /** * General purpose modal dialog. Assumes that: * -- the root tag of the dialog is marked with a unique class name (passed as dlgClass), as well as the * classes "template modal hide". * -- the HTML for the dialog contains elements with "title" and "message" classes, as well as a number * of elements with "dialog-button" class, each of which has a "data-button-id". * * @param {string} dlgClass The class of the dialog node in the HTML. * @param {string=} title The title of the error dialog. Can contain HTML markup. If unspecified, title in * the HTML template is used unchanged. * @param {string=} message The message to display in the error dialog. Can contain HTML markup. If * unspecified, body in the HTML template is used unchanged. * @return {$.Promise} a promise that will be resolved with the ID of the clicked button when the dialog * is dismissed. Never rejected. */ function showModalDialog(dlgClass, title, message) { var result = $.Deferred(), promise = result.promise(); // We clone the HTML rather than using it directly so that if two dialogs of the same // type happen to show up, they can appear at the same time. (This is an edge case that // shouldn't happen often, but we can't prevent it from happening since everything is // asynchronous.) var $dlg = $("." + dlgClass + ".template") .clone() .removeClass("template") .addClass("instance") .appendTo(window.document.body); if ($dlg.length === 0) { throw new Error("Dialog id " + dlgClass + " does not exist"); } // Save the dialog promise for unit tests $dlg.data("promise", promise); // Set title and message if (title) { $(".dialog-title", $dlg).html(title); } if (message) { $(".dialog-message", $dlg).html(message); } var handleKeyDown = _handleKeyDown.bind($dlg); // Pipe dialog-closing notification back to client code $dlg.one("hidden", function () { var buttonId = $dlg.data("buttonId"); if (!buttonId) { // buttonId will be undefined if closed via Bootstrap's "x" button buttonId = DIALOG_BTN_CANCEL; } // Let call stack return before notifying that dialog has closed; this avoids issue #191 // if the handler we're triggering might show another dialog (as long as there's no // fade-out animation) window.setTimeout(function () { result.resolve(buttonId); }, 0); // Remove the dialog instance from the DOM. $dlg.remove(); // Remove keydown event handler window.document.body.removeEventListener("keydown", handleKeyDown, true); KeyBindingManager.setEnabled(true); }).one("shown", function () { // Set focus to the default button var primaryBtn = $dlg.find(".primary"); if (primaryBtn) { primaryBtn.focus(); } // Listen for dialog keyboard shortcuts window.document.body.addEventListener("keydown", handleKeyDown, true); KeyBindingManager.setEnabled(false); }); // Click handler for buttons $dlg.one("click", ".dialog-button", function (e) { _dismissDialog($dlg, $(this).attr("data-button-id")); }); // Run the dialog $dlg.modal({ backdrop: "static", show: true, keyboard: true }); return promise; } /** * Immediately closes any dialog instances with the given class. The dialog callback for each instance will * be called with the special buttonId DIALOG_CANCELED (note: callback is run asynchronously). */ function cancelModalDialogIfOpen(dlgClass) { $("." + dlgClass + ".instance").each(function (index, dlg) { if ($(dlg).is(":visible")) { // Bootstrap breaks if try to hide dialog that's already hidden _dismissDialog($(dlg), DIALOG_CANCELED); } }); } exports.DIALOG_BTN_CANCEL = DIALOG_BTN_CANCEL; exports.DIALOG_BTN_OK = DIALOG_BTN_OK; exports.DIALOG_BTN_DONTSAVE = DIALOG_BTN_DONTSAVE; exports.DIALOG_CANCELED = DIALOG_CANCELED; exports.DIALOG_BTN_DOWNLOAD = DIALOG_BTN_DOWNLOAD; exports.DIALOG_ID_ERROR = DIALOG_ID_ERROR; exports.DIALOG_ID_SAVE_CLOSE = DIALOG_ID_SAVE_CLOSE; exports.DIALOG_ID_EXT_CHANGED = DIALOG_ID_EXT_CHANGED; exports.DIALOG_ID_EXT_DELETED = DIALOG_ID_EXT_DELETED; exports.DIALOG_ID_LIVE_DEVELOPMENT = DIALOG_ID_LIVE_DEVELOPMENT; exports.DIALOG_ID_ABOUT = DIALOG_ID_ABOUT; exports.DIALOG_ID_UPDATE = DIALOG_ID_UPDATE; exports.showModalDialog = showModalDialog; exports.cancelModalDialogIfOpen = cancelModalDialogIfOpen; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, brackets, $, window */ /** * This is a collection of utility functions for gathering performance data. */ define('utils/PerfUtils',['require','exports','module'],function (require, exports, module) { /** * Flag to enable/disable performance data gathering. Default is true (enabled) * @type {boolean} enabled */ var enabled = true; /** * Performance data is stored in this hash object. The key is the name of the * test (passed to markStart/addMeasurement), and the value is the time, in * milliseconds, that it took to run the test. If multiple runs of the same test * are made, the value is an Array with each run stored as an entry in the Array. */ var perfData = {}; /** * Active tests. This is a hash of all tests that have had markStart() called, * but have not yet had addMeasurement() called. */ var activeTests = {}; /** * Updatable tests. This is a hash of all tests that have had markStart() called, * and have had updateMeasurement() called. Caller must explicitly remove tests * from this list using finalizeMeasurement() */ var updatableTests = {}; /** * Hash of measurement IDs */ var perfMeasurementIds = {}; /** * @private * A unique key to log performance data * * @param {!string} id Unique ID for this measurement name * @param {!name} name A short name for this measurement */ function PerfMeasurement(id, name) { this.id = id; this.name = name; } /** * Create a new PerfMeasurement key. Adds itself to the module export. * Can be accessed on the module, e.g. PerfUtils.MY_PERF_KEY. * * @param {!string} id Unique ID for this measurement name * @param {!name} name A short name for this measurement */ function createPerfMeasurement(id, name) { if (perfMeasurementIds[id]) { throw new Error("Performance measurement " + id + " is already defined"); } var pm = new PerfMeasurement(id, name); exports[id] = pm; return pm; } /** * @private * Convert a PerfMeasurement instance to it's id. Otherwise uses the string name for backwards compatibility. */ function toMeasurementId(o) { return (o instanceof PerfMeasurement) ? o.id : o; } /** * @private * Helper function for markStart() * * @param {string} name Timer name. * @param {number} time Timer start time. */ function _markStart(name, time) { if (activeTests[name]) { throw new Error("Recursive tests with the same name are not supported. Timer name: " + name); } activeTests[name] = { startTime: time }; } /** * Start a new named timer. The name should be as descriptive as possible, since * this name will appear as an entry in the performance report. * For example: "Open file: /Users/brackets/src/ProjectManager.js" * * Multiple timers can be opened simultaneously, but all open timers must have * a unique name. * * @param {(string|Array.<string>)} name Single name or an Array of names. * @returns {string} timer name. Returned for convenience to store and use * for calling addMeasure(). Since name is often creating via concatenating * strings this return value allows clients to construct the name once. */ function markStart(name) { if (!enabled) { return; } var time = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); // Array of names can be passed in to have multiple timers with same start time if (Array.isArray(name)) { var i; for (i = 0; i < name.length; i++) { _markStart(name[i], time); } } else { _markStart(name, time); } return name; } /** * Stop a timer and add its measurements to the performance data. * * Multiple measurements can be stored for any given name. If there are * multiple values for a name, they are stored in an Array. * * If markStart() was not called for the specified timer, the * measured time is relative to app startup. * * @param {string} name Timer name. */ function addMeasurement(name) { if (!enabled) { return; } var elapsedTime = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); if (activeTests[name]) { elapsedTime -= activeTests[name].startTime; delete activeTests[name]; } if (perfData[name]) { // We have existing data, add to it if (Array.isArray(perfData[name])) { perfData[name].push(elapsedTime); } else { // Current data is a number, convert to Array perfData[name] = [perfData[name], elapsedTime]; } } else { perfData[name] = elapsedTime; } // Real time logging //console.log(name + " " + elapsedTime); } /** * This function is similar to addMeasurement(), but it allows timing the * *last* event, when you don't know which event will be the last one. * * Tests that are in the activeTests list, have not yet been added, so add * measurements to the performance data, and move test to updatableTests list. * A test is moved to the updatable list so that it no longer passes isActive(). * * Tests that are already in the updatableTests list are updated. * * Caller must explicitly remove test from the updatableTests list using * finalizeMeasurement(). * * If markStart() was not called for the specified timer, there is no way to * determine if this is the first or subsequent call, so the measurement is * not updatable, and it is handled in addMeasurement(). * * @param {string} name Timer name. */ function updateMeasurement(name) { var elapsedTime = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); if (updatableTests[name]) { // update existing measurement elapsedTime -= updatableTests[name].startTime; // update if (perfData[name] && Array.isArray(perfData[name])) { // We have existing data and it's an array, so update the last entry perfData[name][perfData[name].length - 1] = elapsedTime; } else { // No current data or a single entry, so set/update it perfData[name] = elapsedTime; } } else { // not yet in updatable list if (activeTests[name]) { // save startTime in updatable list before addMeasurement() deletes it updatableTests[name] = { startTime: activeTests[name].startTime }; } // let addMeasurement() handle the initial case addMeasurement(name); } } /** * Remove timer from lists so next action starts a new measurement * * updateMeasurement may not have been called, so timer may be * in either or neither list, but should never be in both. * * @param {string} name Timer name. */ function finalizeMeasurement(name) { name = toMeasurementId(name); if (activeTests[name]) { delete activeTests[name]; } if (updatableTests[name]) { delete updatableTests[name]; } } /** * Returns whether a timer is active or not, where "active" means that * timer has been started with addMark(), but has not been added to perfdata * with addMeasurement(). * * @param {string} name Timer name. * @return {boolean} Whether a timer is active or not. */ function isActive(name) { return (activeTests[name]) ? true : false; } /** * Returns the performance data as a tab deliminted string * @returns {string} */ function getDelimitedPerfData() { var getValue = function (entry) { // return single value, or tab deliminted values for an array if (Array.isArray(entry)) { var i, values = ""; for (i = 0; i < entry.length; i++) { values += entry[i]; if (i < entry.length - 1) { values += ", "; } } return values; } else { return entry; } }; var testName, index, result = ""; $.each(perfData, function (testName, entry) { result += getValue(entry) + "\t" + testName + "\n"; }); return result; } /** * Returns the measured value for the given measurement name. * @param {string|PerfMeasurement} name The measurement to retreive. */ function getData(name) { if (!name) { return perfData; } return perfData[toMeasurementId(name)]; } function searchData(regExp) { var keys = Object.keys(perfData).filter(function (key) { return regExp.test(key); }); var datas = []; keys.forEach(function (key) { datas.push(perfData[key]); }); return datas; } /** * Clear all logs including metric data and active tests. */ function clear() { perfData = {}; activeTests = {}; updatableTests = {}; } // create performance measurement constants createPerfMeasurement("INLINE_EDITOR_OPEN", "Open inline editor"); createPerfMeasurement("INLINE_EDITOR_CLOSE", "Close inline editor"); // extensions may create additional measurement constants during their lifecycle exports.addMeasurement = addMeasurement; exports.finalizeMeasurement = finalizeMeasurement; exports.isActive = isActive; exports.markStart = markStart; exports.getData = getData; exports.searchData = searchData; exports.updateMeasurement = updateMeasurement; exports.getDelimitedPerfData = getDelimitedPerfData; exports.createPerfMeasurement = createPerfMeasurement; exports.clear = clear; }); /** * @license RequireJS i18n 1.0.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/requirejs for details */ /*jslint regexp: false, nomen: false, plusplus: false, strict: false */ /*global require: false, navigator: false, define: false */ /** * This plugin handles i18n! prefixed modules. It does the following: * * 1) A regular module can have a dependency on an i18n bundle, but the regular * module does not want to specify what locale to load. So it just specifies * the top-level bundle, like "i18n!nls/colors". * * This plugin will load the i18n bundle at nls/colors, see that it is a root/master * bundle since it does not have a locale in its name. It will then try to find * the best match locale available in that master bundle, then request all the * locale pieces for that best match locale. For instance, if the locale is "en-us", * then the plugin will ask for the "en-us", "en" and "root" bundles to be loaded * (but only if they are specified on the master bundle). * * Once all the bundles for the locale pieces load, then it mixes in all those * locale pieces into each other, then finally sets the context.defined value * for the nls/colors bundle to be that mixed in locale. * * 2) A regular module specifies a specific locale to load. For instance, * i18n!nls/fr-fr/colors. In this case, the plugin needs to load the master bundle * first, at nls/colors, then figure out what the best match locale is for fr-fr, * since maybe only fr or just root is defined for that locale. Once that best * fit is found, all of its locale pieces need to have their bundles loaded. * * Once all the bundles for the locale pieces load, then it mixes in all those * locale pieces into each other, then finally sets the context.defined value * for the nls/fr-fr/colors bundle to be that mixed in locale. */ (function () { //regexp for reconstructing the master bundle name from parts of the regexp match //nlsRegExp.exec("foo/bar/baz/nls/en-ca/foo") gives: //["foo/bar/baz/nls/en-ca/foo", "foo/bar/baz/nls/", "/", "/", "en-ca", "foo"] //nlsRegExp.exec("foo/bar/baz/nls/foo") gives: //["foo/bar/baz/nls/foo", "foo/bar/baz/nls/", "/", "/", "foo", ""] //so, if match[5] is blank, it means this is the top bundle definition. var nlsRegExp = /(^.*(^|\/)nls(\/|$))([^\/]*)\/?([^\/]*)/, empty = {}; //Helper function to avoid repeating code. Lots of arguments in the //desire to stay functional and support RequireJS contexts without having //to know about the RequireJS contexts. function addPart(locale, master, needed, toLoad, prefix, suffix) { if (master[locale]) { needed.push(locale); if (master[locale] === true || master[locale] === 1) { toLoad.push(prefix + locale + '/' + suffix); } } } function addIfExists(req, locale, toLoad, prefix, suffix) { var fullName = prefix + locale + '/' + suffix; if (require._fileExists(req.toUrl(fullName))) { toLoad.push(fullName); } } /** * Simple function to mix in properties from source into target, * but only if target does not already have a property of the same name. * This is not robust in IE for transferring methods that match * Object.prototype names, but the uses of mixin here seem unlikely to * trigger a problem related to that. */ function mixin(target, source, force) { for (var prop in source) { if (!(prop in empty) && (!(prop in target) || force)) { target[prop] = source[prop]; } } } define('i18n',{ version: '1.0.0', /** * Called when a dependency needs to be loaded. */ load: function (name, req, onLoad, config) { config = config || {}; var masterName, match = nlsRegExp.exec(name), prefix = match[1], locale = match[4], suffix = match[5], parts = locale.split("-"), toLoad = [], value = {}, i, part, current = ""; //If match[5] is blank, it means this is the top bundle definition, //so it does not have to be handled. Locale-specific requests //will have a match[4] value but no match[5] if (match[5]) { //locale-specific bundle prefix = match[1]; masterName = prefix + suffix; } else { //Top-level bundle. masterName = name; suffix = match[4]; locale = config.locale || (config.locale = typeof navigator === "undefined" ? "root" : (navigator.language || navigator.userLanguage || "root").toLowerCase()); parts = locale.split("-"); } if (config.isBuild) { //Check for existence of all locale possible files and //require them if exist. toLoad.push(masterName); addIfExists(req, "root", toLoad, prefix, suffix); for (i = 0; (part = parts[i]); i++) { current += (current ? "-" : "") + part; addIfExists(req, current, toLoad, prefix, suffix); } req(toLoad, function () { onLoad(); }); } else { //First, fetch the master bundle, it knows what locales are available. req([masterName], function (master) { //Figure out the best fit var needed = []; //Always allow for root, then do the rest of the locale parts. addPart("root", master, needed, toLoad, prefix, suffix); for (i = 0; (part = parts[i]); i++) { current += (current ? "-" : "") + part; addPart(current, master, needed, toLoad, prefix, suffix); } //Load all the parts missing. req(toLoad, function () { var i, partBundle; for (i = needed.length - 1; i > -1 && (part = needed[i]); i--) { partBundle = master[part]; if (partBundle === true || partBundle === 1) { partBundle = req(prefix + part + '/' + suffix); } mixin(value, partBundle); } //All done, notify the loader. onLoad(value); }); }); } } }); }()); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('nls/strings',['require','exports','module'],function (require, exports, module) { // Code that needs to display user strings should call require("strings") to load // src/strings.js. This file will dynamically load strings.js for the specified brackets.locale. // // See the README.md file in this folder for information on how to add a new translation for // another language or locale. // // TODO: dynamically populate the local prefix list below? module.exports = { root: true, "de": true, "fr": true }; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('nls/root/strings',{ /** * Errors */ // General file io error strings "GENERIC_ERROR" : "(error {0})", "NOT_FOUND_ERR" : "The file could not be found.", "NOT_READABLE_ERR" : "The file could not be read.", "NO_MODIFICATION_ALLOWED_ERR" : "The target directory cannot be modified.", "NO_MODIFICATION_ALLOWED_ERR_FILE" : "The permissions do not allow you to make modifications.", // Project error strings "ERROR_LOADING_PROJECT" : "Error loading project", "OPEN_DIALOG_ERROR" : "An error occurred when showing the open file dialog. (error {0})", "REQUEST_NATIVE_FILE_SYSTEM_ERROR" : "An error occurred when trying to load the directory <span class='dialog-filename'>{0}</span>. (error {1})", "READ_DIRECTORY_ENTRIES_ERROR" : "An error occurred when reading the contents of the directory <span class='dialog-filename'>{0}</span>. (error {1})", // File open/save error string "ERROR_OPENING_FILE_TITLE" : "Error opening file", "ERROR_OPENING_FILE" : "An error occurred when trying to open the file <span class='dialog-filename'>{0}</span>. {1}", "ERROR_RELOADING_FILE_TITLE" : "Error reloading changes from disk", "ERROR_RELOADING_FILE" : "An error occurred when trying to reload the file <span class='dialog-filename'>{0}</span>. {1}", "ERROR_SAVING_FILE_TITLE" : "Error saving file", "ERROR_SAVING_FILE" : "An error occurred when trying to save the file <span class='dialog-filename'>{0}</span>. {1}", "INVALID_FILENAME_TITLE" : "Invalid file name", "INVALID_FILENAME_MESSAGE" : "Filenames cannot contain the following characters: /?*:;{}<>\\|", "FILE_ALREADY_EXISTS" : "The file <span class='dialog-filename'>{0}</span> already exists.", "ERROR_CREATING_FILE_TITLE" : "Error creating file", "ERROR_CREATING_FILE" : "An error occurred when trying to create the file <span class='dialog-filename'>{0}</span>. {1}", // Application error strings "ERROR_BRACKETS_IN_BROWSER_TITLE" : "Oops! Brackets doesn't run in browsers yet.", "ERROR_BRACKETS_IN_BROWSER" : "Brackets is built in HTML, but right now it runs as a desktop app so you can use it to edit local files. Please use the application shell in the <b>github.com/adobe/brackets-app</b> repo to run Brackets.", // FileIndexManager error string "ERROR_MAX_FILES_TITLE" : "Error Indexing Files", "ERROR_MAX_FILES" : "The maximum number of files have been indexed. Actions that look up files in the index may function incorrectly.", // CSSManager error strings "ERROR_PARSE_TITLE" : "Error parsing CSS file(s):", // Live Development error strings "ERROR_LAUNCHING_BROWSER_TITLE" : "Error launching browser", "ERROR_CANT_FIND_CHROME" : "The Google Chrome browser could not be found. Please make sure it is installed.", "ERROR_LAUNCHING_BROWSER" : "An error occurred when launching the browser. (error {0})", "LIVE_DEVELOPMENT_ERROR_TITLE" : "Live Development Error", "LIVE_DEVELOPMENT_ERROR_MESSAGE" : "A live development connection to Chrome could not be established. For live development to work, Chrome needs to be started with remote debugging enabled.<br /><br />Would you like to relaunch Chrome and enable remote debugging?", "LIVE_DEV_NEED_HTML_MESSAGE" : "Open an HTML file in order to launch live preview.", "LIVE_DEV_STATUS_TIP_NOT_CONNECTED" : "Live File Preview", "LIVE_DEV_STATUS_TIP_PROGRESS1" : "Live File Preview: Connecting...", "LIVE_DEV_STATUS_TIP_PROGRESS2" : "Live File Preview: Initializing...", "LIVE_DEV_STATUS_TIP_CONNECTED" : "Disconnect Live File Preview", "SAVE_CLOSE_TITLE" : "Save Changes", "SAVE_CLOSE_MESSAGE" : "Do you want to save the changes you made in the document <span class='dialog-filename'>{0}</span>?", "SAVE_CLOSE_MULTI_MESSAGE" : "Do you want to save your changes to the following files?", "EXT_MODIFIED_TITLE" : "External Changes", "EXT_MODIFIED_MESSAGE" : "<span class='dialog-filename'>{0}</span> has been modified on disk, but also has unsaved changes in Brackets.<br /><br />Which version do you want to keep?", "EXT_DELETED_MESSAGE" : "<span class='dialog-filename'>{0}</span> has been deleted on disk, but has unsaved changes in Brackets.<br /><br />Do you want to keep your changes?", // Find, Replace, Find in Files "SEARCH_REGEXP_INFO" : "Use /re/ syntax for regexp search", "WITH" : "With", "BUTTON_YES" : "Yes", "BUTTON_NO" : "No", "BUTTON_STOP" : "Stop", "OPEN_FILE" : "Open File", "RELEASE_NOTES" : "Release Notes", "NO_UPDATE_TITLE" : "You're up to date!", "NO_UPDATE_MESSAGE" : "You are running the latest version of Brackets.", // Switch language "LANGUAGE_TITLE" : "Switch Language", "LANGUAGE_MESSAGE" : "Please select the desired language from the list below:", "LANGUAGE_SUBMIT" : "Reload Brackets", "LANGUAGE_CANCEL" : "Cancel", /** * ProjectManager */ "UNTITLED" : "Untitled", /** * Command Name Constants */ // File menu commands "FILE_MENU" : "File", "CMD_FILE_NEW" : "New", "CMD_FILE_OPEN" : "Open\u2026", "CMD_ADD_TO_WORKING_SET" : "Add To Working Set", "CMD_OPEN_FOLDER" : "Open Folder\u2026", "CMD_FILE_CLOSE" : "Close", "CMD_FILE_CLOSE_ALL" : "Close All", "CMD_FILE_SAVE" : "Save", "CMD_FILE_SAVE_ALL" : "Save All", "CMD_LIVE_FILE_PREVIEW" : "Live File Preview", "CMD_QUIT" : "Quit", // Edit menu commands "EDIT_MENU" : "Edit", "CMD_SELECT_ALL" : "Select All", "CMD_FIND" : "Find", "CMD_FIND_IN_FILES" : "Find in Files", "CMD_FIND_NEXT" : "Find Next", "CMD_FIND_PREVIOUS" : "Find Previous", "CMD_REPLACE" : "Replace", "CMD_INDENT" : "Indent", "CMD_UNINDENT" : "Unindent", "CMD_DUPLICATE" : "Duplicate", "CMD_COMMENT" : "Comment/Uncomment Lines", "CMD_LINE_UP" : "Move Line(s) Up", "CMD_LINE_DOWN" : "Move Line(s) Down", // View menu commands "VIEW_MENU" : "View", "CMD_HIDE_SIDEBAR" : "Hide Sidebar", "CMD_SHOW_SIDEBAR" : "Show Sidebar", "CMD_INCREASE_FONT_SIZE" : "Increase Font Size", "CMD_DECREASE_FONT_SIZE" : "Decrease Font Size", "CMD_RESTORE_FONT_SIZE" : "Restore Font Size", // Navigate menu Commands "NAVIGATE_MENU" : "Navigate", "CMD_QUICK_OPEN" : "Quick Open", "CMD_GOTO_LINE" : "Go to Line", "CMD_GOTO_DEFINITION" : "Go to Definition", "CMD_TOGGLE_QUICK_EDIT" : "Quick Edit", "CMD_QUICK_EDIT_PREV_MATCH" : "Previous Match", "CMD_QUICK_EDIT_NEXT_MATCH" : "Next Match", "CMD_NEXT_DOC" : "Next Document", "CMD_PREV_DOC" : "Previous Document", // Debug menu commands "DEBUG_MENU" : "Debug", "CMD_REFRESH_WINDOW" : "Reload Brackets", "CMD_SHOW_DEV_TOOLS" : "Show Developer Tools", "CMD_RUN_UNIT_TESTS" : "Run Tests", "CMD_JSLINT" : "Enable JSLint", "CMD_SHOW_PERF_DATA" : "Show Performance Data", "CMD_NEW_BRACKETS_WINDOW" : "New Brackets Window", "CMD_SHOW_EXTENSIONS_FOLDER" : "Show Extensions Folder", "CMD_USE_TAB_CHARS" : "Use Tab Characters", "CMD_SWITCH_LANGUAGE" : "Switch Language", "CMD_CHECK_FOR_UPDATE" : "Check for Updates", // Help menu commands "CMD_ABOUT" : "About", // Special commands invoked by the native shell "CMD_CLOSE_WINDOW" : "Close Window", "CMD_ABORT_QUIT" : "Abort Quit", // Strings for main-view.html "EXPERIMENTAL_BUILD" : "Experimental Build", "JSLINT_ERRORS" : "JSLint Errors", "SEARCH_RESULTS" : "Search Results", "OK" : "OK", "DONT_SAVE" : "Don't Save", "SAVE" : "Save", "CANCEL" : "Cancel", "RELOAD_FROM_DISK" : "Reload from Disk", "KEEP_CHANGES_IN_EDITOR" : "Keep Changes in Editor", "CLOSE_DONT_SAVE" : "Close (Don't Save)", "RELAUNCH_CHROME" : "Relaunch Chrome", "ABOUT" : "About", "BRACKETS" : "Brackets", "CLOSE" : "Close", "ABOUT_TEXT_LINE1" : "sprint 13 experimental build ", "ABOUT_TEXT_LINE2" : "Copyright 2012 Adobe Systems Incorporated and its licensors. All rights reserved.", "ABOUT_TEXT_LINE3" : "Notices; terms and conditions pertaining to third party software are located at ", "ABOUT_TEXT_LINE4" : " and incorporated by reference herein.", "ABOUT_TEXT_LINE5" : "Documentation and source at ", "UPDATE_NOTIFICATION_TOOLTIP" : "There's a new build of Brackets available! Click here for details.", "UPDATE_AVAILABLE_TITLE" : "Update Available", "UPDATE_MESSAGE" : "Hey, there's a new build of Brackets available. Here are some of the new features:", "GET_IT_NOW" : "Get it now!" }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ /** * This file provides the interface to user visible strings in Brackets. Code that needs * to display strings should should load this module by calling var Strings = require("strings"). * The i18n plugin will dynamically load the strings for the right locale and populate * the exports variable. See src\nls\strings.js for the master file of English strings. */ define('strings',['require','exports','module','i18n!nls/strings'],function (require, exports, module) { module.exports = require("i18n!nls/strings"); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Utilities functions related to string manipulation * */ define('utils/StringUtils',['require','exports','module'],function (require, exports, module) { /** * Format a string by replacing placeholder symbols with passed in arguments. * * Example: var formatted = StringUtils.format("Hello {0}", "World"); * * @param {string} str The base string * @param {...} Arguments to be substituted into the string * * @return {string} Formatted string */ function format(str) { // arguments[0] is the base string, so we need to adjust index values here var args = [].slice.call(arguments, 1); return str.replace(/\{(\d+)\}/g, function (match, num) { return typeof args[num] !== "undefined" ? args[num] : match; }); } function htmlEscape(str) { return String(str) .replace(/&/g, "&") .replace(/"/g, """) .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">"); } function regexEscape(str) { return str.replace(/([.?*+\^$\[\]\\(){}|\-])/g, "\\$1"); } // Periods (aka "dots") are allowed in HTML identifiers, but jQuery interprets // them as the start of a class selector, so they need to be escaped function jQueryIdEscape(str) { return str.replace(/\./g, "\\."); } /** * Splits the text by new line characters and returns an array of lines * @param {string} text * @return {Array.<string>} lines */ function getLines(text) { return text.split("\n"); } /** * Returns a line number corresponding to an offset in some text. The text can * be specified as a single string or as an array of strings that correspond to * the lines of the string. * * Specify the text in lines when repeatedly calling the function on the same * text in a loop. Use getLines() to divide the text into lines, then repeatedly call * this function to compute a line number from the offset. * * @param {string | Array.<string>} textOrLines - string or array of lines from which * to compute the line number from the offset * @param {number} offset * @return {number} line number */ function offsetToLineNum(textOrLines, offset) { if (Array.isArray(textOrLines)) { var lines = textOrLines, total = 0, line; for (line = 0; line < lines.length; line++) { if (total < offset) { // add 1 per line since /n were removed by splitting, but they needed to // contribute to the total offset count total += lines[line].length + 1; } else if (total === offset) { return line; } else { return line - 1; } } // if offset is NOT over the total then offset is in the last line if (offset <= total) { return line - 1; } else { return undefined; } } else { return textOrLines.substr(0, offset).split("\n").length - 1; } } // Define public API exports.format = format; exports.htmlEscape = htmlEscape; exports.regexEscape = regexEscape; exports.jQueryIdEscape = jQueryIdEscape; exports.getLines = getLines; exports.offsetToLineNum = offsetToLineNum; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, FileError, brackets, unescape, window */ /** * Set of utilites for working with files and text content. */ define('file/FileUtils',['require','exports','module','file/NativeFileSystem','utils/PerfUtils','widgets/Dialogs','strings','utils/StringUtils'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PerfUtils = require("utils/PerfUtils"), Dialogs = require("widgets/Dialogs"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), Encodings = NativeFileSystem.Encodings; /** * Asynchronously reads a file as UTF-8 encoded text. * @return {$.Promise} a jQuery promise that will be resolved with the * file's text content plus its timestamp, or rejected with a FileError if * the file can not be read. */ function readAsText(fileEntry) { var result = new $.Deferred(), reader; // Measure performance var perfTimerName = PerfUtils.markStart("readAsText:\t" + fileEntry.fullPath); result.always(function () { PerfUtils.addMeasurement(perfTimerName); }); // Read file reader = new NativeFileSystem.FileReader(); fileEntry.file(function (file) { reader.onload = function (event) { var text = event.target.result; fileEntry.getMetadata( function (metadata) { result.resolve(text, metadata.modificationTime); }, function (error) { result.reject(error); } ); }; reader.onerror = function (event) { result.reject(event.target.error); }; reader.readAsText(file, Encodings.UTF8); }); return result.promise(); } /** * Asynchronously writes a file as UTF-8 encoded text. * @param {!FileEntry} fileEntry * @param {!string} text * @return {$.Promise} a jQuery promise that will be resolved when * file writing completes, or rejected with a FileError. */ function writeText(fileEntry, text) { var result = new $.Deferred(); fileEntry.createWriter(function (fileWriter) { fileWriter.onwriteend = function (e) { result.resolve(); }; fileWriter.onerror = function (err) { result.reject(err); }; // TODO (issue #241): NativeFileSystem.BlobBulder fileWriter.write(text); }); return result.promise(); } /** @const */ var LINE_ENDINGS_CRLF = "CRLF"; /** @const */ var LINE_ENDINGS_LF = "LF"; /** * Returns the standard line endings for the current platform * @return {LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} */ function getPlatformLineEndings() { return brackets.platform === "win" ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF; } /** * Scans the first 1000 chars of the text to determine how it encodes line endings. Returns * null if usage is mixed or if no line endings found. * @param {!string} text * @return {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} */ function sniffLineEndings(text) { var subset = text.substr(0, 1000); // (length is clipped to text.length) var hasCRLF = /\r\n/.test(subset); var hasLF = /[^\r]\n/.test(subset); if ((hasCRLF && hasLF) || (!hasCRLF && !hasLF)) { return null; } else { return hasCRLF ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF; } } /** * Translates any line ending types in the given text to the be the single form specified * @param {!string} text * @param {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} lineEndings * @return {string} */ function translateLineEndings(text, lineEndings) { if (lineEndings !== LINE_ENDINGS_CRLF && lineEndings !== LINE_ENDINGS_LF) { lineEndings = getPlatformLineEndings(); } var eolStr = (lineEndings === LINE_ENDINGS_CRLF ? "\r\n" : "\n"); var findAnyEol = /\r\n|\r|\n/g; return text.replace(findAnyEol, eolStr); } function getFileErrorString(code) { // There are a few error codes that we have specific error messages for. The rest are // displayed with a generic "(error N)" message. var result; if (code === FileError.NOT_FOUND_ERR) { result = Strings.NOT_FOUND_ERR; } else if (code === FileError.NOT_READABLE_ERR) { result = Strings.NOT_READABLE_ERR; } else if (code === FileError.NO_MODIFICATION_ALLOWED_ERR) { result = Strings.NO_MODIFICATION_ALLOWED_ERR_FILE; } else { result = StringUtils.format(Strings.GENERIC_ERROR, code); } return result; } function showFileOpenError(code, path) { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_OPENING_FILE_TITLE, StringUtils.format( Strings.ERROR_OPENING_FILE, StringUtils.htmlEscape(path), getFileErrorString(code) ) ); } /** * Convert a URI path to a native path. * On both platforms, this unescapes the URI * On windows, URI paths start with a "/", but have a drive letter ("C:"). In this * case, remove the initial "/". * @param {!string} path * @return {string} */ function convertToNativePath(path) { path = unescape(path); if (path.indexOf(":") !== -1 && path[0] === "/") { return path.substr(1); } return path; } /** * Returns a native absolute path to the 'brackets' source directory. * Note that this only works when run in brackets/src/index.html, so it does * not work for unit tests (which is run from brackets/test/SpecRunner.html) * @return {string} */ function getNativeBracketsDirectoryPath() { var pathname = decodeURI(window.location.pathname); var directory = pathname.substr(0, pathname.lastIndexOf("/")); return convertToNativePath(directory); } /** * Given the module object passed to JS module define function, * convert the path (which is relative to the current window) * to a native absolute path. * Returns a native absolute path to the module folder. * @return {string} */ function getNativeModuleDirectoryPath(module) { var path, relPath, index, pathname; if (module && module.uri) { // Remove window name from base path. Maintain trailing slash. pathname = decodeURI(window.location.pathname); path = convertToNativePath(pathname.substr(0, pathname.lastIndexOf("/") + 1)); // Remove module name from relative path. Remove trailing slash. pathname = decodeURI(module.uri); relPath = pathname.substr(0, pathname.lastIndexOf("/")); // handle leading "../" in relative directory while (relPath.substr(0, 3) === "../") { path = path.substr(0, path.length - 1); // strip trailing slash from base path index = path.lastIndexOf("/"); // find next slash from end if (index !== -1) { path = path.substr(0, index + 1); // remove last dir while maintaining slash } relPath = relPath.substr(3); // remove leading "../" from relative path } path += relPath; } return path; } // Define public API exports.LINE_ENDINGS_CRLF = LINE_ENDINGS_CRLF; exports.LINE_ENDINGS_LF = LINE_ENDINGS_LF; exports.getPlatformLineEndings = getPlatformLineEndings; exports.sniffLineEndings = sniffLineEndings; exports.translateLineEndings = translateLineEndings; exports.showFileOpenError = showFileOpenError; exports.getFileErrorString = getFileErrorString; exports.readAsText = readAsText; exports.writeText = writeText; exports.convertToNativePath = convertToNativePath; exports.getNativeBracketsDirectoryPath = getNativeBracketsDirectoryPath; exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ define('language/HTMLUtils',['require','exports','module'],function (require, exports, module) { //constants var TAG_NAME = "tagName", ATTR_NAME = "attr.name", ATTR_VALUE = "attr.value"; /** * @private * moves the current context backwards by one token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _movePrevToken(ctx) { if (ctx.pos.ch <= 0 || ctx.token.start <= 0) { //move up a line if (ctx.pos.line <= 0) { return false; //at the top already } ctx.pos.line--; ctx.pos.ch = ctx.editor.getLine(ctx.pos.line).length; } else { ctx.pos.ch = ctx.token.start; } ctx.token = ctx.editor.getTokenAt(ctx.pos); return true; } /** * @private * moves the current context forward by one token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _moveNextToken(ctx) { var eol = ctx.editor.getLine(ctx.pos.line).length; if (ctx.pos.ch >= eol || ctx.token.end >= eol) { //move down a line if (ctx.pos.line >= ctx.editor.lineCount() - 1) { return false; //at the bottom } ctx.pos.line++; ctx.pos.ch = 0; } else { ctx.pos.ch = ctx.token.end + 1; } ctx.token = ctx.editor.getTokenAt(ctx.pos); return true; } /** * @private * moves the current context in the given direction, skipping any whitespace it hits * @param {function} moveFxn the funciton to move the context * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _moveSkippingWhitespace(moveFxn, ctx) { if (!moveFxn(ctx)) { return false; } while (!ctx.token.className && ctx.token.string.trim().length === 0) { if (!moveFxn(ctx)) { return false; } } return true; } /** * @private * creates a context object * @param {CodeMirror} editor * @param {{ch:{string}, line:{number}} pos * @return {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} */ function _getInitialContext(editor, pos) { return { "editor": editor, "pos": pos, "token": editor.getTokenAt(pos) }; } /** * @private * in the given context, get the character offset of pos from the start of the token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {number} */ function _offsetInToken(ctx) { var offset = ctx.pos.ch - ctx.token.start; if (offset < 0) { console.log("CodeHintUtils: _offsetInToken - Invalid context: the pos what not in the current token!"); } return offset; } /** * @private * Sometimes as attr values are getting typed, if the quotes aren't balanced yet * some extra 'non attribute value' text gets included in the token. This attempts * to assure the attribute value we grab is always good * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return { val:{string}, offset:{number}} */ function _extractAttrVal(ctx) { var attrValue = ctx.token.string, startChar = attrValue.charAt(0), endChar = attrValue.charAt(attrValue.length - 1), offset = _offsetInToken(ctx), foundEqualSign = false; //If this is a fully quoted value, return the whole //thing regardless of position if (attrValue.length > 1 && (startChar === "'" || startChar === '"') && endChar === startChar) { // Find an equal sign before the end quote. If found, // then the user may be entering an attribute value right before // another attribute and we're getting a false balanced string. // An example of this case is <link rel" href="foo"> where the // cursor is right after the first double quote. foundEqualSign = (attrValue.match(/\=\s*['"]$/) !== null); if (!foundEqualSign) { //strip the quotes and return; attrValue = attrValue.substring(1, attrValue.length - 1); offset = offset - 1 > attrValue.length ? attrValue.length : offset - 1; return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: true}; } } if (foundEqualSign) { var spaceIndex = attrValue.indexOf(" "); attrValue = attrValue.substring(0, (spaceIndex > offset) ? spaceIndex : offset); } else if (offset > 0) { //The att value it getting edit in progress. There is possible extra //stuff in this token state since the quote isn't closed, so we assume //the stuff from the quote to the current pos is definitely in the attribute //value. attrValue = attrValue.substring(0, offset); } //If the attrValue start with a quote, trim that now startChar = attrValue.charAt(0); if (startChar === "'" || startChar === '"') { attrValue = attrValue.substring(1); offset--; } else { startChar = ""; } return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: false}; } /** * @private * Gets the tagname from where ever you are in the currect state * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _extractTagName(ctx) { if (ctx.token.state.tagName) { return ctx.token.state.tagName; //XML mode } else if (ctx.token.state.htmlState) { return ctx.token.state.htmlState.tagName; //HTML mode } // Some mixed modes that offer HTML as a nested mode don't actually expose the HTML state return null; } /** * Creates a tagInfo object and assures all the values are entered or are empty strings * @param {string} tokenType what is getting edited and should be hinted * @param {number} offset where the cursor is for the part getting hinted * @param {string} tagName The name of the tag * @param {string} attrName The name of the attribute * @param {string} attrValue The value of the attribute * @return {{tagName:string, attr{name:string, value:string}, hint:{type:{string}, offset{number}}}} * A tagInfo object with some context about the current tag hint. */ function createTagInfo(tokenType, offset, tagName, attrName, attrValue, valueAssigned, quoteChar, hasEndQuote) { return { tagName: tagName || "", attr: { name: attrName || "", value: attrValue || "", valueAssigned: valueAssigned || false, quoteChar: quoteChar || "", hasEndQuote: hasEndQuote || false }, position: { tokenType: tokenType || "", offset: offset || 0 } }; } /** * @private * Gets the taginfo starting from the attribute value and moving backwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _getTagInfoStartingFromAttrValue(ctx) { // Assume we in the attr value // and validate that by going backwards var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, offset = attrInfo.offset, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote, strLength = ctx.token.string.length; if (ctx.token.className === "string" && ctx.pos.ch === ctx.token.end && strLength > 1) { var firstChar = ctx.token.string[0], lastChar = ctx.token.string[strLength - 1]; // We get here only when the cursor is immediately on the right of the end quote // of an attribute value. So we want to return an empty tag info so that the caller // can dismiss the code hint popup if it is still open. if (firstChar === lastChar && (firstChar === "'" || firstChar === "\"")) { return createTagInfo(); } } //Move to the prev token, and check if it's "=" if (!_moveSkippingWhitespace(_movePrevToken, ctx) || ctx.token.string !== "=") { return createTagInfo(); } //Move to the prev token, and check if it's an attribute if (!_moveSkippingWhitespace(_movePrevToken, ctx) || ctx.token.className !== "attribute") { return createTagInfo(); } var attrName = ctx.token.string; var tagName = _extractTagName(ctx); //We're good. return createTagInfo(ATTR_VALUE, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * @private * Gets the taginfo starting from the attribute name and moving forwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @param {boolean} isPriorAttr indicates whether we're getting info for a prior attribute * @return {string} */ function _getTagInfoStartingFromAttrName(ctx, isPriorAttr) { //Verify We're in the attribute name, move forward and try to extract the rest of //the info. If the user it typing the attr the rest might not be here if (isPriorAttr === false && ctx.token.className !== "attribute") { return createTagInfo(); } var tagName = _extractTagName(ctx); var attrName = ctx.token.string; var offset = _offsetInToken(ctx); if (!_moveSkippingWhitespace(_moveNextToken, ctx) || ctx.token.string !== "=") { return createTagInfo(ATTR_NAME, offset, tagName, attrName); } if (!_moveSkippingWhitespace(_moveNextToken, ctx)) { return createTagInfo(ATTR_NAME, offset, tagName, attrName); } //this should be the attrvalue var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote; return createTagInfo(ATTR_NAME, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * Figure out if we're in a tag, and if we are return info about what to hint about it * An example token stream for this tag is <span id="open-files-disclosure-arrow"></span> : * className:tag string:"<span" * className: string:" " * className:attribute string:"id" * className: string:"=" * className:string string:""open-files-disclosure-arrow"" * className:tag string:"></span>" * @param {Editor} editor An instance of a Brackets editor * @param {{ch: number, line: number}} constPos A CM pos (likely from editor.getCursor()) * @return {{tagName:string, attr{name:string, value:string}, hint:{type:{string}, offset{number}}}} * A tagInfo object with some context about the current tag hint. */ function getTagInfo(editor, constPos) { //we're going to changing pos a lot, but we don't want to mess up //the pos the caller passed in so we use extend to make a safe copy of it. //This is what pass by value in c++ would do. var pos = $.extend({}, constPos), ctx = _getInitialContext(editor._codeMirror, pos), offset = _offsetInToken(ctx), tagInfo, tokenType; // check if this is inside a style block. if (editor.getModeForSelection() !== "html") { return createTagInfo(); } //check and see where we are in the tag if (ctx.token.string.length > 0 && ctx.token.string.trim().length === 0) { // token at (i.e. before) pos is whitespace, so test token at next pos // // note: getTokenAt() does range checking for ch. If it detects that ch is past // EOL, it uses EOL, same token is returned, and the following condition fails, // so we don't need to worry about testPos being valid. var testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line}, testToken = editor._codeMirror.getTokenAt(testPos); if (testToken.string.length > 0 && testToken.string.trim().length > 0 && testToken.string.charAt(0) !== ">") { // pos has whitespace before it and non-whitespace after it, so use token after ctx.token = testToken; if (ctx.token.className === "tag") { // check to see if the cursor is just before a "<" but not in any tag. if (ctx.token.string.charAt(0) === "<") { return createTagInfo(); } } else if (ctx.token.className === "attribute") { // check to see if the user is going to add a new attr before an existing one return _getTagInfoStartingFromAttrName(ctx, false); } else if (ctx.token.className === "string") { // we're either before a "=" or an attribute value. return createTagInfo(); } ctx.pos = testPos; // Get the new offset from test token and subtract one for testPos adjustment offset = _offsetInToken(ctx) - 1; } else { // We get here if ">" or white spaces after testPos. // next, see what's before pos if (!_movePrevToken(ctx)) { return createTagInfo(); } if (ctx.token.className !== "tag") { //if wasn't the tag name, assume it was an attr value tagInfo = _getTagInfoStartingFromAttrValue(ctx); //if it wasn't an attr value, assume it was an empty attr (ie. attr with no value) if (!tagInfo.tagName) { tagInfo = _getTagInfoStartingFromAttrName(ctx, true); } //We don't want to give context for the previous attr //and we want it to look like the user is going to add a new attr if (tagInfo.tagName) { return createTagInfo(ATTR_NAME, 0, tagInfo.tagName); } return createTagInfo(); } //we know the tag was here, so they user is adding an attr name tokenType = ATTR_NAME; offset = 0; } } if (ctx.token.className === "tag") { // Check if the user just typed a white space after "<" that made an existing tag invalid. if (ctx.token.string.indexOf("< ") === 0) { return createTagInfo(); } //check to see if this is the closing of a tag (either the start or end) if (ctx.token.string === ">" || (ctx.token.string.charAt(0) === "<" && ctx.token.string.charAt(1) === "/")) { return createTagInfo(); } if (!tokenType) { tokenType = TAG_NAME; offset--; //need to take off 1 for the leading "<" } //we're actually in the tag, just return that as we have no relevant //info about what attr is selected return createTagInfo(tokenType, offset, _extractTagName(ctx)); } if (ctx.token.string === "=") { // To discourage unquoted attribute value usage we intentionally return an invalid tag info here. // This will also spare us from handling the conversion between quoted and unquoted attribute values. return createTagInfo(); } if (ctx.token.className === "attribute") { tagInfo = _getTagInfoStartingFromAttrName(ctx, false); } else { // if we're not at a tag, "=", or attribute name, assume we're in the value tagInfo = _getTagInfoStartingFromAttrValue(ctx); } if (tokenType && tagInfo.tagName) { tagInfo.position.tokenType = tokenType; tagInfo.position.offset = offset; } return tagInfo; } /** * Returns an Array of info about all <style> blocks in the given Editor's HTML document (assumes * the Editor contains HTML text). * @param {!Editor} editor */ function findStyleBlocks(editor) { // Start scanning from beginning of file var ctx = _getInitialContext(editor._codeMirror, {line: 0, ch: 0}); var styleBlocks = []; var currentStyleBlock = null; var inStyleBlock = false; while (_moveNextToken(ctx)) { if (inStyleBlock) { // Check for end of this <style> block if (ctx.token.state.mode !== "css") { currentStyleBlock.text = editor.document.getRange(currentStyleBlock.start, currentStyleBlock.end); inStyleBlock = false; } else { currentStyleBlock.end = { line: ctx.pos.line, ch: ctx.pos.ch }; } } else { // Check for start of a <style> block if (ctx.token.state.mode === "css") { currentStyleBlock = { start: { line: ctx.pos.line, ch: ctx.pos.ch } }; styleBlocks.push(currentStyleBlock); inStyleBlock = true; } // else, random token in non-CSS content: ignore } } return styleBlocks; } // Define public API exports.TAG_NAME = TAG_NAME; exports.ATTR_NAME = ATTR_NAME; exports.ATTR_VALUE = ATTR_VALUE; exports.getTagInfo = getTagInfo; //The createTagInfo is really only for the unit tests so they can make the same structure to //compare results with exports.createTagInfo = createTagInfo; exports.findStyleBlocks = findStyleBlocks; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window */ /** * Utilities for managing pop-ups. */ define('widgets/PopUpManager',['require','exports','module','editor/EditorManager'],function (require, exports, module) { var EditorManager = require("editor/EditorManager"); var _popUps = []; /** * Add Esc key handling for a popup DOM element. * * @param {!jQuery} $popUp jQuery object for the DOM element pop-up * @param {function} removeHandler Pop-up specific remove (e.g. display:none or DOM removal) * @param {?Boolean} autoAddRemove - Specify true to indicate the PopUpManager should * add/remove the popup from the DOM when the popup is open/closed. Specify false * when the popup is either always persistant in the DOM or the add/remove is handled * external to the PopupManager * */ function addPopUp($popUp, removeHandler, autoAddRemove) { autoAddRemove = autoAddRemove || false; _popUps.push($popUp[0]); $popUp.data("PopUpManager-autoAddRemove", autoAddRemove); $popUp.data("PopUpManager-removeHandler", removeHandler); if (autoAddRemove) { $(window.document.body).append($popUp); } } /** * Remove Esc key handling for a pop-up. Removes the pop-up from the DOM * if the pop-up is currently visible and was not originally attached. * * @param {!jQuery} $popUp */ function removePopUp($popUp) { var index = _popUps.indexOf($popUp[0]); if (index >= 0) { var autoAddRemove = $popUp.data("PopUpManager-autoAddRemove"), removeHandler = $popUp.data("PopUpManager-removeHandler"); if (removeHandler && $popUp.find(":visible").length > 0) { removeHandler(); } if (autoAddRemove) { $popUp.remove(); _popUps = _popUps.slice(index); } } } function _keydownCaptureListener(keyEvent) { if (keyEvent.keyCode !== 27) { // escape key return; } // allow the popUp to prevent closing var $popUp, i, event = new $.Event("popUpClose"); for (i = _popUps.length - 1; i >= 0; i--) { $popUp = $(_popUps[i]); if ($popUp.find(":visible").length > 0) { $popUp.trigger(event); if (!event.isDefaultPrevented()) { // Stop the DOM event from propagating keyEvent.stopImmediatePropagation(); removePopUp($popUp); // TODO: right now Menus and Context Menus do not take focus away from // the editor. We need to have a focus manager to correctly manage focus // between editors and other UI elements. // For now we don't set focus here and assume individual popups // adjust focus if necessary // See story in Trello card #404 //EditorManager.focusEditor(); } break; } } } window.document.body.addEventListener("keydown", _keydownCaptureListener, true); exports.addPopUp = addPopUp; exports.removePopUp = removePopUp; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ define('utils/ViewUtils',['require','exports','module'],function (require, exports, module) { var SCROLL_SHADOW_HEIGHT = 5; /** * @private */ var _resizeHandlers = []; /** * Positions shadow background elements to indicate vertical scrolling. * @param {!DOMElement} $displayElement the DOMElement that displays the shadow * @param {!Object} $scrollElement the object that is scrolled * @param {!DOMElement} $shadowTop div .scroller-shadow.top * @param {!DOMElement} $shadowBottom div .scroller-shadow.bottom * @param {boolean} isPositionFixed When using absolute position, top remains at 0. */ function _updateScrollerShadow($displayElement, $scrollElement, $shadowTop, $shadowBottom, isPositionFixed) { var offsetTop = 0, scrollElement = $scrollElement.get(0), scrollTop = scrollElement.scrollTop, topShadowOffset = Math.min(scrollTop - SCROLL_SHADOW_HEIGHT, 0), displayElementWidth = $displayElement.width(); if ($shadowTop) { $shadowTop.css("background-position", "0px " + topShadowOffset + "px"); if (isPositionFixed) { offsetTop = $displayElement.offset().top; $shadowTop.css("top", offsetTop); } if (isPositionFixed) { $shadowTop.css("width", displayElementWidth); } } if ($shadowBottom) { var clientHeight = scrollElement.clientHeight, outerHeight = $displayElement.outerHeight(), scrollHeight = scrollElement.scrollHeight, bottomOffset = outerHeight - clientHeight, bottomShadowOffset = SCROLL_SHADOW_HEIGHT; // outside of shadow div viewport if (scrollHeight > clientHeight) { bottomShadowOffset -= Math.min(SCROLL_SHADOW_HEIGHT, (scrollHeight - (scrollTop + clientHeight))); } $shadowBottom.css("background-position", "0px " + bottomShadowOffset + "px"); $shadowBottom.css("top", offsetTop + outerHeight - SCROLL_SHADOW_HEIGHT); $shadowBottom.css("width", displayElementWidth); } } function getOrCreateShadow($displayElement, position, isPositionFixed) { var $findShadow = $displayElement.find(".scroller-shadow." + position); if ($findShadow.length === 0) { $findShadow = $(window.document.createElement("div")).addClass("scroller-shadow " + position); $displayElement.append($findShadow); } if (!isPositionFixed) { // position is fixed by default $findShadow.css("position", "absolute"); $findShadow.css(position, "0"); } return $findShadow; } /** * Installs event handlers for updatng shadow background elements to indicate vertical scrolling. * @param {!DOMElement} displayElement the DOMElement that displays the shadow. Must fire * "contentChanged" events when the element is resized or repositioned. * @param {?Object} scrollElement the object that is scrolled. Must fire "scroll" events * when the element is scrolled. If null, the displayElement is used. * @param {?boolean} showBottom optionally show the bottom shadow */ function addScrollerShadow(displayElement, scrollElement, showBottom) { // use fixed positioning when the display and scroll elements are the same var isPositionFixed = false; if (!scrollElement) { scrollElement = displayElement; isPositionFixed = true; } // update shadows when the scrolling element is scrolled var $displayElement = $(displayElement), $scrollElement = $(scrollElement); var $shadowTop = getOrCreateShadow($displayElement, "top", isPositionFixed); var $shadowBottom = (showBottom) ? getOrCreateShadow($displayElement, "bottom", isPositionFixed) : null; var doUpdate = function () { _updateScrollerShadow($displayElement, $scrollElement, $shadowTop, $shadowBottom, isPositionFixed); }; $scrollElement.on("scroll.scroller-shadow", doUpdate); $displayElement.on("contentChanged.scroller-shadow", doUpdate); // update immediately doUpdate(); } /** * Remove scroller-shadow effect. * @param {!DOMElement} displayElement the DOMElement that displays the shadow * @param {?Object} scrollElement the object that is scrolled */ function removeScrollerShadow(displayElement, scrollElement) { if (!scrollElement) { scrollElement = displayElement; } var $displayElement = $(displayElement), $scrollElement = $(scrollElement); // remove scrollerShadow elements from DOM $displayElement.find(".scroller-shadow.top").remove(); $displayElement.find(".scroller-shadow.bottom").remove(); // remove event handlers $scrollElement.off("scroll.scroller-shadow"); $displayElement.off("contentChanged.scroller-shadow"); } /** * Within a scrolling DOMElement, creates and positions a styled selection * div to align a single selected list item from a ul list element. * * Assumptions: * - scrollElement is a child of the #file-section div * - ul list element fires a "selectionChanged" event after the * selectedClassName is assigned to a new list item * * @param {!DOMElement} scrollElement A DOMElement containing a ul list element * @param {!string} selectedClassName A CSS class name on at most one list item in the contained list */ function sidebarList($scrollerElement, selectedClassName, leafClassName) { var $listElement = $scrollerElement.find("ul"), $selectionMarker, $selectionTriangle, $sidebar = $("#sidebar"), showTriangle = true; // build selectionMarker and position absolute within the scroller $selectionMarker = $(window.document.createElement("div")).addClass("sidebar-selection"); $scrollerElement.prepend($selectionMarker); // enable scrolling $scrollerElement.css("overflow", "auto"); // use relative postioning for clipping the selectionMarker within the scrollElement $scrollerElement.css("position", "relative"); // build selectionTriangle and position fixed to the window $selectionTriangle = $(window.document.createElement("div")).addClass("sidebar-selection-triangle"); $scrollerElement.append($selectionTriangle); selectedClassName = "." + (selectedClassName || "selected"); var updateSelectionTriangle = function () { var selectionMarkerHeight = $selectionMarker.height(), selectionMarkerOffset = $selectionMarker.offset(), scrollerOffset = $scrollerElement.offset(), triangleHeight = $selectionTriangle.outerHeight(), scrollerTop = scrollerOffset.top, scrollerBottom = scrollerTop + $scrollerElement.outerHeight(), scrollerLeft = scrollerOffset.left, triangleTop = selectionMarkerOffset.top; $selectionTriangle.css("top", triangleTop); $selectionTriangle.css("left", $sidebar.width() - $selectionTriangle.outerWidth()); $selectionTriangle.toggleClass("triangle-visible", showTriangle); var triangleClipOffsetYBy = Math.floor((selectionMarkerHeight - triangleHeight) / 2), triangleBottom = triangleTop + triangleHeight + triangleClipOffsetYBy; if (triangleTop < scrollerTop || triangleBottom > scrollerBottom) { $selectionTriangle.css("clip", "rect(" + Math.max(scrollerTop - triangleTop - triangleClipOffsetYBy, 0) + "px, auto, " + (triangleHeight - Math.max(triangleBottom - scrollerBottom, 0)) + "px, auto)"); } else { $selectionTriangle.css("clip", ""); } }; var updateSelectionMarker = function (event, reveal) { // find the selected list item var $listItem = $listElement.find(selectedClassName).closest("li"); if (leafClassName) { showTriangle = $listItem.hasClass(leafClassName); } // always hide selection visuals first to force layout (issue #719) $selectionTriangle.hide(); $selectionMarker.hide(); if ($listItem.length === 1) { // list item position is relative to scroller var selectionMarkerTop = $listItem.offset().top - $scrollerElement.offset().top + $scrollerElement.get(0).scrollTop; // force selection width to match scroller $selectionMarker.width($scrollerElement.get(0).scrollWidth); // move the selectionMarker position to align with the list item $selectionMarker.css("top", selectionMarkerTop); $selectionMarker.show(); updateSelectionTriangle(); $selectionTriangle.show(); // fully scroll to the selectionMarker if it's not initially in the viewport var scrollerElement = $scrollerElement.get(0), scrollerHeight = scrollerElement.clientHeight, selectionMarkerHeight = $selectionMarker.height(), selectionMarkerBottom = selectionMarkerTop + selectionMarkerHeight, currentScrollBottom = scrollerElement.scrollTop + scrollerHeight; // update scrollTop to reveal the selected list item if (reveal) { if (selectionMarkerTop >= currentScrollBottom) { $listItem.get(0).scrollIntoView(false); } else if (selectionMarkerBottom <= scrollerElement.scrollTop) { $listItem.get(0).scrollIntoView(true); } } } }; $listElement.on("selectionChanged", updateSelectionMarker); $scrollerElement.on("scroll", updateSelectionTriangle); // update immediately updateSelectionMarker(); // update clipping when the window resizes _resizeHandlers.push(updateSelectionTriangle); } /** * @private */ function _handleResize() { _resizeHandlers.forEach(function (f) { f.apply(); }); } /** * Within a scrolling DOMElement, if necessary, scroll element into viewport. * * To Perform the minimum amount of scrolling necessary, cases should be handled as follows: * - element already completely in view : no scrolling * - element above viewport : scroll view so element is at top * - element left of viewport : scroll view so element is at left * - element below viewport : scroll view so element is at bottom * - element right of viewport : scroll view so element is at right * * Assumptions: * - $view is a scrolling container * * @param {!DOMElement} $view - A jQuery scrolling container * @param {!DOMElement} $element - A jQuery element * @param {?boolean} scrollHorizontal - whether to also scroll horizonally */ function scrollElementIntoView($view, $element, scrollHorizontal) { var viewOffset = $view.offset(), viewScroller = $view.get(0), element = $element.get(0), elementOffset = $element.offset(); // scroll minimum amount if (elementOffset.top + $element.height() >= (viewOffset.top + $view.height())) { // below viewport element.scrollIntoView(false); } else if (elementOffset.top <= viewOffset.top) { // above viewport element.scrollIntoView(true); } if (scrollHorizontal) { if (elementOffset.left < 0) { $view.scrollLeft($view.scrollLeft() + elementOffset.left); } else if (elementOffset.left + $element.width() >= viewOffset.left + $view.width()) { $view.scrollLeft(elementOffset.left - viewOffset.left); } } } // handle all resize handlers in a single listener $(window).resize(_handleResize); // Define public API exports.SCROLL_SHADOW_HEIGHT = SCROLL_SHADOW_HEIGHT; exports.addScrollerShadow = addScrollerShadow; exports.removeScrollerShadow = removeScrollerShadow; exports.sidebarList = sidebarList; exports.scrollElementIntoView = scrollElementIntoView; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window, brackets */ define('editor/CodeHintManager',['require','exports','module','language/HTMLUtils','command/Menus','utils/StringUtils','editor/EditorManager','widgets/PopUpManager','utils/ViewUtils'],function (require, exports, module) { // Load dependent modules var HTMLUtils = require("language/HTMLUtils"), Menus = require("command/Menus"), StringUtils = require("utils/StringUtils"), EditorManager = require("editor/EditorManager"), PopUpManager = require("widgets/PopUpManager"), ViewUtils = require("utils/ViewUtils"); var hintProviders = [], hintList, shouldShowHintsOnKeyUp = false; /** * @constructor * * Displays a popup list of code completions. * Currently only HTML tags are supported, but this will greatly be extended in coming sprint * to include: extensibility API, HTML attributes hints, JavaScript hints, CSS hints */ function CodeHintList() { this.currentProvider = null; this.query = {queryStr: null}; this.displayList = []; this.options = { maxResults: 999 }; this.opened = false; this.selectedIndex = -1; this.editor = null; this.$hintMenu = $("<li class='dropdown codehint-menu'></li>"); var $toggle = $("<a href='#' class='dropdown-toggle'></a>") .hide(); this.$hintMenu.append($toggle) .append("<ul class='dropdown-menu'></ul>"); } /** * @private * Enters the code completion text into the editor * @string {string} completion - text to insert into current code editor */ CodeHintList.prototype._handleItemClick = function (completion) { this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos()); this.close(); }; /** * Adds a single item to the hint list * @param {string} name */ CodeHintList.prototype.addItem = function (name) { var self = this; var displayName = name.replace( new RegExp(StringUtils.regexEscape(this.query.queryStr), "i"), "<strong>$&</strong>" ); var $item = $("<li><a href='#'><span class='codehint-item'>" + displayName + "</span></a></li>") .on("click", function (e) { // Don't let the click propagate upward (otherwise it will hit the close handler in // bootstrap-dropdown). e.stopPropagation(); self._handleItemClick(name); }); this.$hintMenu.find("ul.dropdown-menu") .append($item); }; /** * Rebuilds the hint list based on this.query */ CodeHintList.prototype.updateList = function () { this.displayList = this.currentProvider.search(this.query); this.buildListView(); }; /** * Removes all list items from hint list */ CodeHintList.prototype.clearList = function () { this.$hintMenu.find("li").remove(); }; /** * Rebuilds the list items for the hint list based on this.displayList */ CodeHintList.prototype.buildListView = function () { this.clearList(); var self = this; var count = 0; $.each(this.displayList, function (index, item) { if (count > self.options.maxResults) { return false; } self.addItem(item); count++; }); if (count === 0) { this.close(); } else { // Select the first item in the list this.setSelectedIndex(0); } }; /** * Selects the item in the hint list specified by index * @param {Number} index */ CodeHintList.prototype.setSelectedIndex = function (index) { var items = this.$hintMenu.find("li"); // Range check index = Math.max(0, Math.min(index, items.length - 1)); // Clear old highlight if (this.selectedIndex !== -1) { $(items[this.selectedIndex]).find("a").removeClass("highlight"); } // Highlight the new selected item this.selectedIndex = index; if (this.selectedIndex !== -1) { var $item = $(items[this.selectedIndex]); var $view = this.$hintMenu.find("ul.dropdown-menu"); ViewUtils.scrollElementIntoView($view, $item, false); $item.find("a").addClass("highlight"); } }; /** * Handles key presses when the hint list is being displayed * @param {Editor} editor * @param {KeyBoardEvent} keyEvent */ CodeHintList.prototype.handleKeyEvent = function (editor, event) { var keyCode = event.keyCode; // Up arrow, down arrow and enter key are always handled here if (event.type !== "keypress" && (keyCode === 38 || keyCode === 40 || keyCode === 13 || keyCode === 33 || keyCode === 34)) { if (event.type === "keydown") { if (keyCode === 38) { // Up arrow this.setSelectedIndex(this.selectedIndex - 1); } else if (keyCode === 40) { // Down arrow this.setSelectedIndex(this.selectedIndex + 1); } else if (keyCode === 33) { // Page Up this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage()); } else if (keyCode === 34) { // Page Down this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage()); } else { // Enter/return key // Trigger a click handler to commmit the selected item $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click"); } } event.preventDefault(); return; } // All other key events trigger a rebuild of the list, but only // on keyup events if (event.type !== "keyup") { return; } this.query = this.currentProvider.getQueryInfo(this.editor, this.editor.getCursorPos()); this.updateList(); // Update the CodeHintList location if (this.displayList.length) { var hintPos = this.calcHintListLocation(); this.$hintMenu.css({"left": hintPos.left, "top": hintPos.top}); } }; /** * Return true if the CodeHintList is open. * @return {Boolean} */ CodeHintList.prototype.isOpen = function () { // We don't get a notification when the dropdown closes. The best // we can do is keep an "opened" flag and check to see if we // still have the "open" class applied. if (this.opened && !this.$hintMenu.hasClass("open")) { this.opened = false; } return this.opened; }; /** * Displays the hint list at the current cursor position * @param {Editor} editor */ CodeHintList.prototype.open = function (editor) { var self = this; this.editor = editor; Menus.closeAll(); this.currentProvider = null; $.each(hintProviders, function (index, item) { var query = item.getQueryInfo(self.editor, self.editor.getCursorPos()); if (query.queryStr !== null) { self.query = query; self.currentProvider = item; return false; } }); if (!this.currentProvider) { return; } this.updateList(); if (this.displayList.length) { // Need to add the menu to the DOM before trying to calculate its ideal location. $("#codehint-menu-bar > ul").append(this.$hintMenu); var hintPos = this.calcHintListLocation(); this.$hintMenu.addClass("open") .css({"left": hintPos.left, "top": hintPos.top}); this.opened = true; PopUpManager.addPopUp(this.$hintMenu, function () { self.close(); }); } }; /** * Closes the hint list */ CodeHintList.prototype.close = function () { // TODO: Due to #1381, this won't get called if the user clicks out of the code hint menu. // That's (sort of) okay right now since it doesn't really matter if a single old invisible // code hint list is lying around (it'll get closed the next time the user pops up a code // hint). Once #1381 is fixed this issue should go away. this.$hintMenu.removeClass("open"); this.opened = false; this.currentProvider = null; PopUpManager.removePopUp(this.$hintMenu); this.$hintMenu.remove(); if (hintList === this) { hintList = null; } }; /** * Computes top left location for hint list so that the list is not clipped by the window * @return {Object.<left: Number, top: Number> } */ CodeHintList.prototype.calcHintListLocation = function () { var cursor = this.editor._codeMirror.cursorCoords(), posTop = cursor.y, posLeft = cursor.x, $window = $(window), $menuWindow = this.$hintMenu.children("ul"); // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code // adjust positioning so menu is not clipped off bottom or right var bottomOverhang = posTop + 25 + $menuWindow.height() - $window.height(); if (bottomOverhang > 0) { posTop -= (27 + $menuWindow.height()); } // todo: should be shifted by line height posTop -= 15; // shift top for hidden parent element //posLeft += 5; var rightOverhang = posLeft + $menuWindow.width() - $window.width(); if (rightOverhang > 0) { posLeft = Math.max(0, posLeft - rightOverhang); } return {left: posLeft, top: posTop}; }; /** * @private * Calculate the number of items per scroll page. Used for PageUp and PageDown. * @return {number} */ CodeHintList.prototype.getItemsPerPage = function () { var itemsPerPage = 1, $items = this.$hintMenu.find("li"), $view = this.$hintMenu.find("ul.dropdown-menu"), itemHeight; if ($items.length !== 0) { itemHeight = $($items[0]).height(); if (itemHeight) { // round down to integer value itemsPerPage = Math.floor($view.height() / itemHeight); itemsPerPage = Math.max(1, Math.min(itemsPerPage, $items.length)); } } return itemsPerPage; }; /** * Show the code hint list near the current cursor position for the specified editor * @param {Editor} */ function showHint(editor) { if (hintList) { hintList.close(); } hintList = new CodeHintList(); hintList.open(editor); } /** * Handles keys related to displaying, searching, and navigating the hint list * @param {Editor} editor * @param {KeyboardEvent} event */ function handleKeyEvent(editor, event) { var provider = null; // Check for Control+Space if (event.type === "keydown" && event.keyCode === 32 && event.ctrlKey) { showHint(editor); event.preventDefault(); } else if (event.type === "keypress") { // Check if any provider wants to show hints on this key. $.each(hintProviders, function (index, item) { if (item.shouldShowHintsOnKey(String.fromCharCode(event.charCode))) { provider = item; return false; } }); shouldShowHintsOnKeyUp = !!provider; } else if (event.type === "keyup") { if (shouldShowHintsOnKeyUp) { shouldShowHintsOnKeyUp = false; showHint(editor); } } // Pass to the hint list, if it's open if (hintList && hintList.isOpen()) { shouldShowHintsOnKeyUp = false; hintList.handleKeyEvent(editor, event); } } /** * Registers an object that is able to provide code hints. When the user requests a code * hint getQueryInfo() will be called on every hint provider. Providers should examine * the state of the editor and return a search query object with a filter string if hints * can be provided. search() will then be called allowing the hint provider to create a * list of hints for the search query. When the user chooses a hint handleSelect() is called * so that the hint provider can insert the hint into the editor. * * @param {Object.< getQueryInfo: function(editor, cursor), * search: function(string), * handleSelect: function(string, Editor, cursor), * shouldShowHintsOnKey: function(string)>} * * Parameter Details: * - getQueryInfo - examines cursor location of editor and returns an object representing * the search query to be used for hinting. queryStr is a required property of the search object * and a client may provide other properties on the object to carry more context about the query. * - search - takes a query object and returns an array of hint strings based on the queryStr property * of the query object. * - handleSelect - takes a completion string and inserts it into the editor near the cursor * position * - shouldShowHintsOnKey - inspects the char code and returns true if it wants to show code hints on that key. */ function registerHintProvider(providerInfo) { hintProviders.push(providerInfo); } /** * Expose CodeHintList for unit testing */ function _getCodeHintList() { return hintList; } // Define public API exports.handleKeyEvent = handleKeyEvent; exports.showHint = showHint; exports._getCodeHintList = _getCodeHintList; exports.registerHintProvider = registerHintProvider; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror */ /** */ define('document/TextRange',['require','exports','module'],function (require, exports, module) { /** * @constructor * * Stores a range of lines that is automatically maintained as the Document changes. The range * MAY drop out of sync with the Document in certain edge cases; startLine & endLine will become * null when that happens. * * Important: you must dispose() a TextRange when you're done with it. Because TextRange addRef()s * the Document (in order to listen to it), you will leak Documents otherwise. * * TextRange dispatches two events: * - change -- When the range changes (due to a Document change) * - lostSync -- When the backing Document changes in such a way that the range can no longer * accurately be maintained. Generally, occurs whenever an edit spans a range boundary. * After this, startLine & endLine will be unusable (set to null). * These events only ever occur in response to Document changes, so if you are already listening * to the Document, you could ignore the TextRange events and just read its updated value in your * own Document change handler. * * @param {!Document} document * @param {number} startLine First line in range (0-based, inclusive) * @param {number} endLine Last line in range (0-based, inclusive) */ function TextRange(document, startLine, endLine) { this.startLine = startLine; this.endLine = endLine; this.document = document; document.addRef(); // store this-bound version of listener so we can remove them later this._handleDocumentChange = this._handleDocumentChange.bind(this); $(document).on("change", this._handleDocumentChange); } /** Detaches from the Document. The TextRange will no longer update or send change events */ TextRange.prototype.dispose = function (editor, change) { // Disconnect from Document this.document.releaseRef(); $(this.document).off("change", this._handleDocumentChange); }; /** @type {!Document} */ TextRange.prototype.document = null; /** @type {?number} Null after "lostSync" is dispatched */ TextRange.prototype.startLine = null; /** @type {?number} Null after "lostSync" is dispatched */ TextRange.prototype.endLine = null; /** * Applies a single Document change object (out of the linked list of multiple such objects) * to this range. Returns true if the range was changed as a result. */ TextRange.prototype._applySingleChangeToRange = function (change) { // console.log(this + " applying change to (" + // (change.from && (change.from.line+","+change.from.ch)) + " - " + // (change.to && (change.to.line+","+change.to.ch)) + ")"); // Special case: the range is no longer meaningful since the entire text was replaced if (!change.from || !change.to) { this.startLine = null; this.endLine = null; return true; // Special case: certain changes around the edges of the range are problematic, because // if they're undone, we'll be unable to determine how to fix up the range to include the // undone content. (The "undo" will just look like an insertion outside our bounds.) So // in those cases, we destroy the range instead of fixing it up incorrectly. The specific // cases are: // 1. Edit crosses the start boundary of the inline editor (defined as character 0 // of the first line). // 2. Edit crosses the end boundary of the inline editor (defined as the newline at // the end of the last line). // Note: we also used to disallow edits that start at the beginning of the range (character 0 // of the first line) if they crossed a newline. This was a vestige from before case #1 // was added; now that edits crossing the top boundary (actually, undos of such edits) are // out of the picture, edits on the first line of the range unambiguously belong inside it. } else if ((change.from.line < this.startLine && change.to.line >= this.startLine) || (change.from.line <= this.endLine && change.to.line > this.endLine)) { this.startLine = null; this.endLine = null; return true; // Normal case: update the range end points if any content was added before them. Note that // we don't rely on line handles for this since we want to gracefully handle cases where the // start or end line was deleted during a change. } else { var numAdded = change.text.length - (change.to.line - change.from.line + 1); var hasChanged = false; // This logic is so simple because we've already excluded all cases where the change // crosses the range boundaries if (change.to.line < this.startLine) { this.startLine += numAdded; hasChanged = true; } if (change.to.line <= this.endLine) { this.endLine += numAdded; hasChanged = true; } // console.log("Now " + this); return hasChanged; } }; /** * Updates the range based on the changeList from a Document "change" event. Dispatches a * "change" event if the range was adjusted at all. Dispatches a "lostSync" event instead if the * range can no longer be accurately maintained. */ TextRange.prototype._applyChangesToRange = function (changeList) { var hasChanged = false; var change; for (change = changeList; change; change = change.next) { // Apply this step of the change list var result = this._applySingleChangeToRange(change); hasChanged = hasChanged || result; // If we lost sync with the range, just bail now if (this.startLine === null || this.endLine === null) { $(this).triggerHandler("lostSync"); break; } } if (hasChanged) { $(this).triggerHandler("change"); } }; TextRange.prototype._handleDocumentChange = function (event, doc, changeList) { this._applyChangesToRange(changeList); }; /* (pretty toString(), to aid debugging) */ TextRange.prototype.toString = function () { return "[TextRange " + this.startLine + "-" + this.endLine + " in " + this.document + "]"; }; // Define public API exports.TextRange = TextRange; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ /** * Editor is a 1-to-1 wrapper for a CodeMirror editor instance. It layers on Brackets-specific * functionality and provides APIs that cleanly pass through the bits of CodeMirror that the rest * of our codebase may want to interact with. An Editor is always backed by a Document, and stays * in sync with its content; because Editor keeps the Document alive, it's important to always * destroy() an Editor that's going away so it can release its Document ref. * * For now, there's a distinction between the "master" Editor for a Document - which secretly acts * as the Document's internal model of the text state - and the multitude of "slave" secondary Editors * which, via Document, sync their changes to and from that master. * * For now, direct access to the underlying CodeMirror object is still possible via _codeMirror -- * but this is considered deprecated and may go away. * * The Editor object dispatches the following events: * - keyEvent -- When any key event happens in the editor (whether it changes the text or not). * Event handlers are passed ({Editor}, {KeyboardEvent}). The 2nd arg is the raw DOM event. * Note: most listeners will only want to respond when event.type === "keypress". * - cursorActivity -- When the user moves the cursor or changes the selection, or an edit occurs. * Note: do not listen to this in order to be generally informed of edits--listen to the * "change" event on Document instead. * - scroll -- When the editor is scrolled, either by user action or programmatically. * - lostContent -- When the backing Document changes in such a way that this Editor is no longer * able to display accurate text. This occurs if the Document's file is deleted, or in certain * Document->editor syncing edge cases that we do not yet support (the latter cause will * eventually go away). * * The Editor also dispatches "change" events internally, but you should listen for those on * Documents, not Editors. * * These are jQuery events, so to listen for them you do something like this: * $(editorInstance).on("eventname", handler); */ define('editor/Editor',['require','exports','module','editor/EditorManager','editor/CodeHintManager','command/Commands','command/CommandManager','command/Menus','utils/PerfUtils','strings','document/TextRange','utils/ViewUtils'],function (require, exports, module) { var EditorManager = require("editor/EditorManager"), CodeHintManager = require("editor/CodeHintManager"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), Menus = require("command/Menus"), PerfUtils = require("utils/PerfUtils"), Strings = require("strings"), TextRange = require("document/TextRange").TextRange, ViewUtils = require("utils/ViewUtils"); /** * @private * Handle Tab key press. * @param {!CodeMirror} instance CodeMirror instance. */ function _handleTabKey(instance) { // Tab key handling is done as follows: // 1. If the selection is before any text and the indentation is to the left of // the proper indentation then indent it to the proper place. Otherwise, // add another tab. In either case, move the insertion point to the // beginning of the text. // 2. If the selection is after the first non-space character, and is not an // insertion point, indent the entire line(s). // 3. If the selection is after the first non-space character, and is an // insertion point, insert a tab character or the appropriate number // of spaces to pad to the nearest tab boundary. var from = instance.getCursor(true), to = instance.getCursor(false), line = instance.getLine(from.line), indentAuto = false, insertTab = false; if (from.line === to.line) { if (line.search(/\S/) > to.ch || to.ch === 0) { indentAuto = true; } } if (indentAuto) { var currentLength = line.length; CodeMirror.commands.indentAuto(instance); // If the amount of whitespace didn't change, insert another tab if (instance.getLine(from.line).length === currentLength) { insertTab = true; to.ch = 0; } } else if (instance.somethingSelected()) { CodeMirror.commands.indentMore(instance); } else { insertTab = true; } if (insertTab) { if (instance.getOption("indentWithTabs")) { CodeMirror.commands.insertTab(instance); } else { var i, ins = "", numSpaces = instance.getOption("tabSize"); numSpaces -= to.ch % numSpaces; for (i = 0; i < numSpaces; i++) { ins += " "; } instance.replaceSelection(ins, "end"); } } } /** * @private * Handle left arrow, right arrow, backspace and delete keys when soft tabs are used. * @param {!CodeMirror} instance CodeMirror instance * @param {number} direction Direction of movement: 1 for forward, -1 for backward * @param {function} functionName name of the CodeMirror function to call * @return {boolean} true if key was handled */ function _handleSoftTabNavigation(instance, direction, functionName) { var handled = false; if (!instance.getOption("indentWithTabs")) { var cursor = instance.getCursor(), tabSize = instance.getOption("tabSize"), jump = cursor.ch % tabSize, line = instance.getLine(cursor.line); if (direction === 1) { jump = tabSize - jump; if (cursor.ch + jump > line.length) { // Jump would go beyond current line return false; } if (line.substr(cursor.ch, jump).search(/\S/) === -1) { instance[functionName](jump, "char"); handled = true; } } else { // Quick exit if we are at the beginning of the line if (cursor.ch === 0) { return false; } // If we are on the tab boundary, jump by the full amount, // but not beyond the start of the line. if (jump === 0) { jump = tabSize; } // Search backwards to the first non-space character var offset = line.substr(cursor.ch - jump, jump).search(/\s*$/g); if (offset !== -1) { // Adjust to jump to first non-space character jump -= offset; } if (jump > 0) { instance[functionName](-jump, "char"); handled = true; } } } return handled; } /** * Checks if the user just typed a closing brace/bracket/paren, and considers automatically * back-indenting it if so. */ function _checkElectricChars(jqEvent, editor, event) { var instance = editor._codeMirror; if (event.type === "keypress") { var keyStr = String.fromCharCode(event.which || event.keyCode); if (/[\]\{\}\)]/.test(keyStr)) { // If all text before the cursor is whitespace, auto-indent it var cursor = instance.getCursor(); var lineStr = instance.getLine(cursor.line); var nonWS = lineStr.search(/\S/); if (nonWS === -1 || nonWS >= cursor.ch) { // Need to do the auto-indent on a timeout to ensure // the keypress is handled before auto-indenting. // This is the same timeout value used by the // electricChars feature in CodeMirror. window.setTimeout(function () { instance.indentLine(cursor.line); }, 75); } } } } function _handleKeyEvents(jqEvent, editor, event) { _checkElectricChars(jqEvent, editor, event); // Pass the key event to the code hint manager. It may call preventDefault() on the event. CodeHintManager.handleKeyEvent(editor, event); } function _handleSelectAll() { var editor = EditorManager.getFocusedEditor(); if (editor) { editor._selectAllVisible(); } } /** * List of all current (non-destroy()ed) Editor instances. Needed when changing global preferences * that affect all editors, e.g. tabbing or color scheme settings. * @type {Array.<Editor>} */ var _instances = []; /** @type {boolean} Global setting: When inserting new text, use tab characters? (instead of spaces) */ var _useTabChar = false; /** * @constructor * * Creates a new CodeMirror editor instance bound to the given Document. The Document need not have * a "master" Editor realized yet, even if makeMasterEditor is false; in that case, the first time * an edit occurs we will automatically ask EditorManager to create a "master" editor to render the * Document modifiable. * * ALWAYS call destroy() when you are done with an Editor - otherwise it will leak a Document ref. * * @param {!Document} document * @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master" * Editor for the Document. If false, this Editor will attach to the Document as a "slave"/ * secondary editor. * @param {!string} mode Syntax-highlighting language mode; "" means plain-text mode. * See {@link EditorUtils#getModeFromFileExtension()}. * @param {!jQueryObject} container Container to add the editor to. * @param {!Object<string, function(Editor)>} additionalKeys Mapping of keyboard shortcuts to * custom handler functions. Mapping is in CodeMirror format * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document * to display in this editor. Inclusive. */ function Editor(document, makeMasterEditor, mode, container, additionalKeys, range) { var self = this; _instances.push(this); // Attach to document: add ref & handlers this.document = document; document.addRef(); if (range) { // attach this first: want range updated before we process a change this._visibleRange = new TextRange(document, range.startLine, range.endLine); } // store this-bound version of listeners so we can remove them later this._handleDocumentChange = this._handleDocumentChange.bind(this); this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this); $(document).on("change", this._handleDocumentChange); $(document).on("deleted", this._handleDocumentDeleted); // (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized) this._inlineWidgets = []; // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { "Tab": _handleTabKey, "Shift-Tab": "indentLess", "Left": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "moveH")) { CodeMirror.commands.goCharLeft(instance); } }, "Right": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "moveH")) { CodeMirror.commands.goCharRight(instance); } }, "Backspace": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "deleteH")) { CodeMirror.commands.delCharLeft(instance); } }, "Delete": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "deleteH")) { CodeMirror.commands.delCharRight(instance); } }, "Esc": function (instance) { self.removeAllInlineWidgets(); }, "Shift-Delete": "cut", "Ctrl-Insert": "copy", "Shift-Insert": "paste" }; EditorManager.mergeExtraKeys(self, codeMirrorKeyMap, additionalKeys); // We'd like null/"" to mean plain text mode. CodeMirror defaults to plaintext for any // unrecognized mode, but it complains on the console in that fallback case: so, convert // here so we're always explicit, avoiding console noise. if (!mode) { mode = "text/plain"; } // Create the CodeMirror instance // (note: CodeMirror doesn't actually require using 'new', but jslint complains without it) this._codeMirror = new CodeMirror(container, { electricChars: false, // we use our own impl of this to avoid CodeMirror bugs; see _checkElectricChars() indentUnit: 4, indentWithTabs: _useTabChar, lineNumbers: true, matchBrackets: true, dragDrop: false, // work around issue #1123 extraKeys: codeMirrorKeyMap }); this._installEditorListeners(); $(this) .on("keyEvent", _handleKeyEvents) .on("change", this._handleEditorChange.bind(this)); // Set code-coloring mode BEFORE populating with text, to avoid a flash of uncolored text this._codeMirror.setOption("mode", mode); // Initially populate with text. This will send a spurious change event, so need to make // sure this is understood as a 'sync from document' case, not a genuine edit this._duringSync = true; this._resetText(document.getText()); this._duringSync = false; if (range) { // Hide all lines other than those we want to show. We do this rather than trimming the // text itself so that the editor still shows accurate line numbers. this._codeMirror.operation(function () { var i; for (i = 0; i < range.startLine; i++) { self._hideLine(i); } var lineCount = self.lineCount(); for (i = range.endLine + 1; i < lineCount; i++) { self._hideLine(i); } }); this.setCursorPos(range.startLine, 0); } // Now that we're fully initialized, we can point the document back at us if needed if (makeMasterEditor) { document._makeEditable(this); } // Add scrollTop property to this object for the scroll shadow code to use Object.defineProperty(this, "scrollTop", { get: function () { return this._codeMirror.getScrollInfo().y; } }); } /** * Removes this editor from the DOM and detaches from the Document. If this is the "master" * Editor that is secretly providing the Document's backing state, then the Document reverts to * a read-only string-backed mode. */ Editor.prototype.destroy = function () { // CodeMirror docs for getWrapperElement() say all you have to do is "Remove this from your // tree to delete an editor instance." $(this.getRootElement()).remove(); _instances.splice(_instances.indexOf(this), 1); // Disconnect from Document this.document.releaseRef(); $(this.document).off("change", this._handleDocumentChange); $(this.document).off("deleted", this._handleDocumentDeleted); if (this._visibleRange) { // TextRange also refs the Document this._visibleRange.dispose(); } // If we're the Document's master editor, disconnecting from it has special meaning if (this.document._masterEditor === this) { this.document._makeNonEditable(); } // Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks // run, at least, since they may also need to release Document refs this._inlineWidgets.forEach(function (inlineWidget) { inlineWidget.onClosed(); }); }; /** * Handles Select All specially when we have a visible range in order to work around * bugs in CodeMirror when lines are hidden. */ Editor.prototype._selectAllVisible = function () { var startLine = this.getFirstVisibleLine(), endLine = this.getLastVisibleLine(); this.setSelection({line: startLine, ch: 0}, {line: endLine, ch: this.document.getLine(endLine).length}); }; Editor.prototype._applyChanges = function (changeList) { var self = this; // _visibleRange has already updated via its own Document listener. See if this change caused // it to lose sync. If so, our whole view is stale - signal our owner to close us. if (this._visibleRange) { if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) { $(self).triggerHandler("lostContent"); return; } } // Apply text changes to CodeMirror editor var cm = this._codeMirror; cm.operation(function () { var change, newText; for (change = changeList; change; change = change.next) { newText = change.text.join('\n'); if (!change.from || !change.to) { if (change.from || change.to) { console.assert(false, "Change record received with only one end undefined--replacing entire text"); } cm.setValue(newText); } else { cm.replaceRange(newText, change.from, change.to); } } }); // The update above may have inserted new lines - must hide any that fall outside our range if (self._visibleRange) { cm.operation(function () { // TODO: could make this more efficient by only iterating across the min-max line // range of the union of all changes var i; for (i = 0; i < cm.lineCount(); i++) { if (i < self._visibleRange.startLine || i > self._visibleRange.endLine) { self._hideLine(i); } else { // Double-check that the set of NON-hidden lines matches our range too console.assert(!cm.getLineHandle(i).hidden); } } }); } }; /** * Responds to changes in the CodeMirror editor's text, syncing the changes to the Document. * There are several cases where we want to ignore a CodeMirror change: * - if we're the master editor, editor changes can be ignored because Document is already listening * for our changes * - if we're a secondary editor, editor changes should be ignored if they were caused by us reacting * to a Document change */ Editor.prototype._handleEditorChange = function (event, editor, changeList) { // we're currently syncing from the Document, so don't echo back TO the Document if (this._duringSync) { return; } // Secondary editor: force creation of "master" editor backing the model, if doesn't exist yet this.document._ensureMasterEditor(); if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; if we got here, this was a real editor change (not a // sync from the real ground truth), so we need to sync from us into the document // (which will directly push the change into the master editor). // FUTURE: Technically we should add a replaceRange() method to Document and go through // that instead of talking to its master editor directly. It's not clear yet exactly // what the right Document API would be, though. this._duringSync = true; this.document._masterEditor._applyChanges(changeList); this._duringSync = false; } // Else, Master editor: // we're the ground truth; nothing else to do, since Document listens directly to us // note: this change might have been a real edit made by the user, OR this might have // been a change synced from another editor if (this._visibleRange) { // _visibleRange has already updated via its own Document listener, when we pushed our // change into the Document above (_masterEditor._applyChanges()). But changes due to our // own edits should never cause the range to lose sync - verify that. if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) { throw new Error("ERROR: Typing in Editor should not destroy its own _visibleRange"); } } }; /** * Responds to changes in the Document's text, syncing the changes into our CodeMirror instance. * There are several cases where we want to ignore a Document change: * - if we're the master editor, Document changes should be ignored becuase we already have the right * text (either the change originated with us, or it has already been set into us by Document) * - if we're a secondary editor, Document changes should be ignored if they were caused by us sending * the document an editor change that originated with us */ Editor.prototype._handleDocumentChange = function (event, doc, changeList) { var change; // we're currently syncing to the Document, so don't echo back FROM the Document if (this._duringSync) { return; } if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; and if we got here, this was a Document change that // didn't come from us (e.g. a sync from another editor, a direct programmatic change // to the document, or a sync from external disk changes)... so sync from the Document this._duringSync = true; this._applyChanges(changeList); this._duringSync = false; } // Else, Master editor: // we're the ground truth; nothing to do since Document change is just echoing our // editor changes }; /** * Responds to the Document's underlying file being deleted. The Document is now basically dead, * so we must close. */ Editor.prototype._handleDocumentDeleted = function () { $(this).triggerHandler("lostContent"); }; /** * Install singleton event handlers on the CodeMirror instance, translating them into multi- * listener-capable jQuery events on the Editor instance. */ Editor.prototype._installEditorListeners = function () { var self = this; // FUTURE: if this list grows longer, consider making this a more generic mapping // NOTE: change is a "private" event--others shouldn't listen to it on Editor, only on // Document this._codeMirror.setOption("onChange", function (instance, changeList) { $(self).triggerHandler("change", [self, changeList]); }); this._codeMirror.setOption("onKeyEvent", function (instance, event) { $(self).triggerHandler("keyEvent", [self, event]); return event.defaultPrevented; // false tells CodeMirror we didn't eat the event }); this._codeMirror.setOption("onCursorActivity", function (instance) { $(self).triggerHandler("cursorActivity", [self]); }); this._codeMirror.setOption("onScroll", function (instance) { // close all dropdowns on scroll Menus.closeAll(); $(self).triggerHandler("scroll", [self]); // notify all inline widgets of a position change self._fireWidgetOffsetTopChanged(self.getFirstVisibleLine() - 1); }); }; /** * Sets the contents of the editor and clears the undo/redo history. Dispatches a change event. * Semi-private: only Document should call this. * @param {!string} text */ Editor.prototype._resetText = function (text) { var perfTimerName = PerfUtils.markStart("Edtitor._resetText()\t" + (!this.document || this.document.file.fullPath)); var cursorPos = this.getCursorPos(), scrollPos = this.getScrollPos(); // This *will* fire a change event, but we clear the undo immediately afterward this._codeMirror.setValue(text); // Make sure we can't undo back to the empty state before setValue() this._codeMirror.clearHistory(); // restore cursor and scroll positions this.setCursorPos(cursorPos); this.setScrollPos(scrollPos.x, scrollPos.y); PerfUtils.addMeasurement(perfTimerName); }; /** * Gets the current cursor position within the editor. If there is a selection, returns whichever * end of the range the cursor lies at. * @return !{line:number, ch:number} */ Editor.prototype.getCursorPos = function () { return this._codeMirror.getCursor(); }; /** * Sets the cursor position within the editor. Removes any selection. * @param {number} line The 0 based line number. * @param {number=} ch The 0 based character position; treated as 0 if unspecified. */ Editor.prototype.setCursorPos = function (line, ch) { this._codeMirror.setCursor(line, ch); }; /** * Given a position, returns its index within the text (assuming \n newlines) * @param {!{line:number, ch:number}} * @return {number} */ Editor.prototype.indexFromPos = function (coords) { return this._codeMirror.indexFromPos(coords); }; /** * Returns true if pos is between start and end (inclusive at both ends) * @param {{line:number, ch:number}} pos * @param {{line:number, ch:number}} start * @param {{line:number, ch:number}} end * */ Editor.prototype.posWithinRange = function (pos, start, end) { var startIndex = this.indexFromPos(start), endIndex = this.indexFromPos(end), posIndex = this.indexFromPos(pos); return posIndex >= startIndex && posIndex <= endIndex; }; /** * @return {boolean} True if there's a text selection; false if there's just an insertion point */ Editor.prototype.hasSelection = function () { return this._codeMirror.somethingSelected(); }; /** * Gets the current selection. Start is inclusive, end is exclusive. If there is no selection, * returns the current cursor position as both the start and end of the range (i.e. a selection * of length zero). * @return {!{start:{line:number, ch:number}, end:{line:number, ch:number}}} */ Editor.prototype.getSelection = function () { var selStart = this._codeMirror.getCursor(true), selEnd = this._codeMirror.getCursor(false); return { start: selStart, end: selEnd }; }; /** * @return {!string} The currently selected text, or "" if no selection. Includes \n if the * selection spans multiple lines (does NOT reflect the Document's line-endings style). */ Editor.prototype.getSelectedText = function () { return this._codeMirror.getSelection(); }; /** * Sets the current selection. Start is inclusive, end is exclusive. Places the cursor at the * end of the selection range. * @param {!{line:number, ch:number}} start * @param {!{line:number, ch:number}} end */ Editor.prototype.setSelection = function (start, end) { this._codeMirror.setSelection(start, end); }; /** * Selects word that the given pos lies within or adjacent to. If pos isn't touching a word * (e.g. within a token like "//"), moves the cursor to pos without selecting a range. * @param {!{line:number, ch:number}} */ Editor.prototype.selectWordAt = function (pos) { this._codeMirror.selectWordAt(pos); }; /** * Gets the total number of lines in the the document (includes lines not visible in the viewport) * @returns {!number} */ Editor.prototype.lineCount = function () { return this._codeMirror.lineCount(); }; /** * Gets the number of the first visible line in the editor. * @returns {number} The 0-based index of the first visible line. */ Editor.prototype.getFirstVisibleLine = function () { return (this._visibleRange ? this._visibleRange.startLine : 0); }; /** * Gets the number of the last visible line in the editor. * @returns {number} The 0-based index of the last visible line. */ Editor.prototype.getLastVisibleLine = function () { return (this._visibleRange ? this._visibleRange.endLine : this.lineCount() - 1); }; // FUTURE change to "hideLines()" API that hides a range of lines at once in a single operation, then fires offsetTopChanged afterwards. /* Hides the specified line number in the editor * @param {!number} */ Editor.prototype._hideLine = function (lineNumber) { var value = this._codeMirror.hideLine(lineNumber); // when this line is hidden, notify all following inline widgets of a position change this._fireWidgetOffsetTopChanged(lineNumber); return value; }; /** * Gets the total height of the document in pixels (not the viewport) * @param {!boolean} includePadding * @returns {!number} height in pixels */ Editor.prototype.totalHeight = function (includePadding) { return this._codeMirror.totalHeight(includePadding); }; /** * Gets the scroller element from the editor. * @returns {!HTMLDivElement} scroller */ Editor.prototype.getScrollerElement = function () { return this._codeMirror.getScrollerElement(); }; /** * Gets the root DOM node of the editor. * @returns {Object} The editor's root DOM node. */ Editor.prototype.getRootElement = function () { return this._codeMirror.getWrapperElement(); }; /** * Gets the lineSpace element within the editor (the container around the individual lines of code). * FUTURE: This is fairly CodeMirror-specific. Logic that depends on this may break if we switch * editors. * @returns {Object} The editor's lineSpace element. */ Editor.prototype._getLineSpaceElement = function () { return $(".CodeMirror-lines", this.getScrollerElement()).children().get(0); }; /** * Returns the current scroll position of the editor. * @returns {{x:number, y:number}} The x,y scroll position in pixels */ Editor.prototype.getScrollPos = function () { return this._codeMirror.getScrollInfo(); }; /** * Sets the current scroll position of the editor. * @param {number} x scrollLeft position in pixels * @param {number} y scrollTop position in pixels */ Editor.prototype.setScrollPos = function (x, y) { this._codeMirror.scrollTo(x, y); }; /** * Adds an inline widget below the given line. If any inline widget was already open for that * line, it is closed without warning. * @param {!{line:number, ch:number}} pos Position in text to anchor the inline. * @param {!InlineWidget} inlineWidget The widget to add. */ Editor.prototype.addInlineWidget = function (pos, inlineWidget) { var self = this; inlineWidget.id = this._codeMirror.addInlineWidget(pos, inlineWidget.htmlContent, inlineWidget.height, function (id) { self._removeInlineWidgetInternal(id); inlineWidget.onClosed(); }); this._inlineWidgets.push(inlineWidget); inlineWidget.onAdded(); // once this widget is added, notify all following inline widgets of a position change this._fireWidgetOffsetTopChanged(pos.line); }; /** * Removes all inline widgets */ Editor.prototype.removeAllInlineWidgets = function () { // copy the array because _removeInlineWidgetInternal will modifying the original var widgets = [].concat(this.getInlineWidgets()); widgets.forEach(function (widget) { this.removeInlineWidget(widget); }, this); }; /** * Removes the given inline widget. * @param {number} inlineWidget The widget to remove. */ Editor.prototype.removeInlineWidget = function (inlineWidget) { var lineNum = this._getInlineWidgetLineNumber(inlineWidget); // _removeInlineWidgetInternal will get called from the destroy callback in CodeMirror. this._codeMirror.removeInlineWidget(inlineWidget.id); // once this widget is removed, notify all following inline widgets of a position change this._fireWidgetOffsetTopChanged(lineNum); }; /** * Cleans up the given inline widget from our internal list of widgets. * @param {number} inlineId id returned by addInlineWidget(). */ Editor.prototype._removeInlineWidgetInternal = function (inlineId) { var i; var l = this._inlineWidgets.length; for (i = 0; i < l; i++) { if (this._inlineWidgets[i].id === inlineId) { this._inlineWidgets.splice(i, 1); break; } } }; /** * Returns a list of all inline widgets currently open in this editor. Each entry contains the * inline's id, and the data parameter that was passed to addInlineWidget(). * @return {!Array.<{id:number, data:Object}>} */ Editor.prototype.getInlineWidgets = function () { return this._inlineWidgets; }; /** * Sets the height of an inline widget in this editor. * @param {!InlineWidget} inlineWidget The widget whose height should be set. * @param {!number} height The height of the widget. * @param {boolean} ensureVisible Whether to scroll the entire widget into view. */ Editor.prototype.setInlineWidgetHeight = function (inlineWidget, height, ensureVisible) { var info = this._codeMirror.getInlineWidgetInfo(inlineWidget.id), oldHeight = (info && info.height) || 0; this._codeMirror.setInlineWidgetHeight(inlineWidget.id, height, ensureVisible); // update position for all following inline editors if (oldHeight !== height) { var lineNum = this._getInlineWidgetLineNumber(inlineWidget); this._fireWidgetOffsetTopChanged(lineNum); } }; /** * @private * Get the starting line number for an inline widget. * @param {!InlineWidget} inlineWidget * @return {number} The line number of the widget or -1 if not found. */ Editor.prototype._getInlineWidgetLineNumber = function (inlineWidget) { var info = this._codeMirror.getInlineWidgetInfo(inlineWidget.id); return (info && info.line) || -1; }; /** * @private * Fire "offsetTopChanged" events when inline editor positions change due to * height changes of other inline editors. * @param {!InlineWidget} inlineWidget */ Editor.prototype._fireWidgetOffsetTopChanged = function (lineNum) { var self = this, otherLineNum; this.getInlineWidgets().forEach(function (other) { otherLineNum = self._getInlineWidgetLineNumber(other); if (otherLineNum > lineNum) { $(other).triggerHandler("offsetTopChanged"); } }); }; /** Gives focus to the editor control */ Editor.prototype.focus = function () { this._codeMirror.focus(); }; /** Returns true if the editor has focus */ Editor.prototype.hasFocus = function () { // The CodeMirror instance wrapper has a "CodeMirror-focused" class set when focused return $(this.getScrollerElement()).hasClass("CodeMirror-focused"); }; /** * Re-renders the editor UI */ Editor.prototype.refresh = function () { this._codeMirror.refresh(); }; /** * Re-renders the editor, and all children inline editors. */ Editor.prototype.refreshAll = function () { this.refresh(); this.getInlineWidgets().forEach(function (multilineEditor, i, arr) { multilineEditor.sizeInlineWidgetToContents(true); multilineEditor._updateRelatedContainer(); multilineEditor.editors.forEach(function (editor, j, arr) { editor.refresh(); }); }); }; /** * Shows or hides the editor within its parent. Does not force its ancestors to * become visible. * @param {boolean} show true to show the editor, false to hide it */ Editor.prototype.setVisible = function (show) { $(this.getRootElement()).css("display", (show ? "" : "none")); this._codeMirror.refresh(); if (show) { this._inlineWidgets.forEach(function (inlineWidget) { inlineWidget.onParentShown(); }); } }; /** * Returns true if the editor is fully visible--i.e., is in the DOM, all ancestors are * visible, and has a non-zero width/height. */ Editor.prototype.isFullyVisible = function () { return $(this.getRootElement()).is(":visible"); }; /** * Gets the syntax-highlighting mode for the current selection or cursor position. (The mode may * vary within one file due to embedded languages, e.g. JS embedded in an HTML script block). * * Returns null if the mode at the start of the selection differs from the mode at the end - * an *approximation* of whether the mode is consistent across the whole range (a pattern like * A-B-A would return A as the mode, not null). * * @return {?string} Name of syntax-highlighting mode; see {@link EditorUtils#getModeFromFileExtension()}. */ Editor.prototype.getModeForSelection = function () { var sel = this.getSelection(); // Check for mixed mode info (meaning mode varies depending on position) // TODO (#921): this only works for certain mixed modes; some do not expose this info var startState = this._codeMirror.getTokenAt(sel.start).state; if (startState.mode) { var startMode = startState.mode; // If mixed mode, check that mode is the same at start & end of selection if (sel.start.line !== sel.end.line || sel.start.ch !== sel.end.ch) { var endState = this._codeMirror.getTokenAt(sel.end).state; var endMode = endState.mode; if (startMode !== endMode) { return null; } } return startMode; } else { // Mode does not vary: just use the editor-wide mode return this._codeMirror.getOption("mode"); } }; /** * The Document we're bound to * @type {!Document} */ Editor.prototype.document = null; /** * If true, we're in the middle of syncing to/from the Document. Used to ignore spurious change * events caused by us (vs. change events caused by others, which we need to pay attention to). * @type {!boolean} */ Editor.prototype._duringSync = false; /** * @private * NOTE: this is actually "semi-private": EditorManager also accesses this field... as well as * a few other modules. However, we should try to gradually move most code away from talking to * CodeMirror directly. * @type {!CodeMirror} */ Editor.prototype._codeMirror = null; /** * @private * @type {!Array.<{id:number, data:Object}>} */ Editor.prototype._inlineWidgets = null; /** * @private * @type {?TextRange} */ Editor.prototype._visibleRange = null; // Global settings that affect all Editor instances (both currently open Editors as well as those created // in the future) /** * Sets whether to use tab characters (vs. spaces) when inserting new text. Affects all Editors. * @param {boolean} value */ Editor.setUseTabChar = function (value) { _useTabChar = value; _instances.forEach(function (editor) { editor._codeMirror.setOption("indentWithTabs", _useTabChar); }); }; /** @type {boolean} Gets whether all Editors use tab characters (vs. spaces) when inserting new text */ Editor.getUseTabChar = function (value) { return _useTabChar; }; // Global commands that affect the currently focused Editor instance, wherever it may be CommandManager.register(Strings.CMD_SELECT_ALL, Commands.EDIT_SELECT_ALL, _handleSelectAll); // Define public API exports.Editor = Editor; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ define('editor/InlineWidget',['require','exports','module','editor/EditorManager'],function (require, exports, module) { // Load dependent modules var EditorManager = require("editor/EditorManager"); /** * @constructor * */ function InlineWidget() { var self = this; // create the outer wrapper div this.htmlContent = window.document.createElement("div"); this.$htmlContent = $(this.htmlContent).addClass("inline-widget"); this.$htmlContent.append("<div class='shadow top' />") .append("<div class='shadow bottom' />"); this.$htmlContent.on("keydown", function (e) { if (e.keyCode === 27) { self.close(); e.stopImmediatePropagation(); } }); } InlineWidget.prototype.htmlContent = null; InlineWidget.prototype.$htmlContent = null; InlineWidget.prototype.id = null; InlineWidget.prototype.hostEditor = null; /** * Initial height of inline widget in pixels. Can be changed later via hostEditor.setInlineWidgetHeight() * @type {number} */ InlineWidget.prototype.height = 0; /** * Closes this inline widget and all its contained Editors */ InlineWidget.prototype.close = function () { var shouldMoveFocus = this._editorHasFocus(); EditorManager.closeInlineWidget(this.hostEditor, this, shouldMoveFocus); // closeInlineWidget() causes our onClosed() handler to be called }; /** * Called any time inline is closed, whether manually or automatically */ InlineWidget.prototype.onClosed = function () { // do nothing - base implementation }; /** * Called once content is parented in the host editor's DOM. Useful for performing tasks like setting * focus or measuring content, which require htmlContent to be in the DOM tree. */ InlineWidget.prototype.onAdded = function () { // do nothing - base implementation }; /** * @param {Editor} hostEditor */ InlineWidget.prototype.load = function (hostEditor) { this.hostEditor = hostEditor; // TODO: incomplete impelementation. It's not clear yet if InlineTextEditor // will fuction as an abstract class or as generic inline editor implementation // that just shows a range of text. See CSSInlineEditor.css for an implementation of load() }; /** * Called when the editor containing the inline is made visible. */ InlineWidget.prototype.onParentShown = function () { // do nothing - base implementation }; exports.InlineWidget = InlineWidget; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ // FUTURE: Merge part (or all) of this class with MultiRangeInlineEditor /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ define('editor/InlineTextEditor',['require','exports','module','document/DocumentManager','editor/EditorManager','command/CommandManager','command/Commands','editor/InlineWidget'],function (require, exports, module) { // Load dependent modules var DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), InlineWidget = require("editor/InlineWidget").InlineWidget; /** * Returns editor holder width (not CodeMirror's width). * @private */ function _editorHolderWidth() { return $("#editor-holder").width(); } /** * Shows or hides the dirty indicator * @private */ function _showDirtyIndicator($indicatorDiv, isDirty) { // Show or hide the dirty indicator by adjusting // the width of the div. $indicatorDiv.css("width", isDirty ? 16 : 0); } /** * Respond to dirty flag change event. If the dirty flag is associated with an inline editor, * show (or hide) the dirty indicator. * @private */ function _dirtyFlagChangeHandler(event, doc) { var $dirtyIndicators = $(".inlineEditorHolder .dirty-indicator"), $indicator; $.each($dirtyIndicators, function (index, indicator) { $indicator = $(indicator); if ($indicator.data("fullPath") === doc.file.fullPath) { _showDirtyIndicator($indicator, doc.isDirty); } }); } /** * @constructor * @extends {InlineWidget} */ function InlineTextEditor() { InlineWidget.call(this); /* @type {Array.<{Editor}>}*/ this.editors = []; } InlineTextEditor.prototype = new InlineWidget(); InlineTextEditor.prototype.constructor = InlineTextEditor; InlineTextEditor.prototype.parentClass = InlineWidget.prototype; InlineTextEditor.prototype.editors = null; /** * Given a host editor and its inline editors, find the widest gutter and make all the others match * @param {!Editor} hostEditor Host editor containing all the inline editors to sync * @private */ function _syncGutterWidths(hostEditor) { var allHostedEditors = EditorManager.getInlineEditors(hostEditor); // add the host itself to the list too allHostedEditors.push(hostEditor); var maxWidth = 0; allHostedEditors.forEach(function (editor) { var $gutter = $(editor._codeMirror.getGutterElement()); $gutter.css("min-width", ""); var curWidth = $gutter.width(); if (curWidth > maxWidth) { maxWidth = curWidth; } }); if (allHostedEditors.length === 1) { //There's only the host, just bail allHostedEditors[0]._codeMirror.setOption("gutter", true); return; } maxWidth = maxWidth + "px"; allHostedEditors.forEach(function (editor) { $(editor._codeMirror.getGutterElement()).css("min-width", maxWidth); editor._codeMirror.setOption("gutter", true); }); } /** * Called any time inline was closed, whether manually (via close()) or automatically */ InlineTextEditor.prototype.onClosed = function () { _syncGutterWidths(this.hostEditor); this.editors.forEach(function (editor) { editor.destroy(); //release ref on Document }); }; /** * Update the inline editor's height when the number of lines change * @param {boolean} force the editor to resize */ InlineTextEditor.prototype.sizeInlineWidgetToContents = function (force) { var i, len = this.editors.length, editor; // TODO: only handles 1 editor right now. Add multiple editor support when // the design is finalized // Reize the editors to the content for (i = 0; i < len; i++) { // Only supports 1 editor right now if (i === 1) { break; } editor = this.editors[i]; if (editor.isFullyVisible()) { var height = editor.totalHeight(true); if (force || height !== this.height) { $(editor.getScrollerElement()).height(height); this.height = height; editor.refresh(); } } } }; /** * Some tasks have to wait until we've been parented into the outer editor * @param {string} the inline ID that is generated by CodeMirror after the widget that holds the inline * editor is constructed and added to the DOM */ InlineTextEditor.prototype.onAdded = function () { this.editors.forEach(function (editor) { editor.refresh(); }); _syncGutterWidths(this.hostEditor); // Set initial size // Note that the second argument here (ensureVisibility) is only used by CSSInlineEditor. // FUTURE: Should clean up this API so it's consistent between the two. this.sizeInlineWidgetToContents(true, true); this.editors[0].focus(); }; /** * * @param {Document} doc * @param {number} startLine of text to show in inline editor * @param {number} endLine of text to show in inline editor * @param {HTMLDivElement} container container to hold the inline editor */ InlineTextEditor.prototype.createInlineEditorFromText = function (doc, startLine, endLine, container, additionalKeys) { var self = this; var range = { startLine: startLine, endLine: endLine }; // root container holding header & editor var $wrapperDiv = $("<div/>"); var wrapperDiv = $wrapperDiv[0]; // header containing filename, dirty indicator, line number var $header = $("<div/>").addClass("inline-editor-header"); var $filenameInfo = $("<a/>").addClass("filename"); // dirty indicator, with file path stored on it var $dirtyIndicatorDiv = $("<div/>") .addClass("dirty-indicator") .width(0); // initialize indicator as hidden $dirtyIndicatorDiv.data("fullPath", doc.file.fullPath); var $lineNumber = $("<span class='line-number'>" + (startLine + 1) + "</span>"); // wrap filename & line number in clickable link with tooltip $filenameInfo.append($dirtyIndicatorDiv) .append(doc.file.name + " : ") .append($lineNumber) .attr("title", doc.file.fullPath); // clicking filename jumps to full editor view $filenameInfo.click(function () { CommandManager.execute(Commands.FILE_OPEN, { fullPath: doc.file.fullPath }) .done(function () { EditorManager.getCurrentFullEditor().setCursorPos(startLine); }); }); $header.append($filenameInfo); $wrapperDiv.append($header); // Create actual Editor instance var inlineInfo = EditorManager.createInlineEditorForDocument(doc, range, wrapperDiv, additionalKeys); this.editors.push(inlineInfo.editor); container.appendChild(wrapperDiv); // Size editor to content whenever it changes (via edits here or any other view of the doc) $(inlineInfo.editor).on("change", function () { self.sizeInlineWidgetToContents(); // And update line number since a change to the Editor equals a change to the Document, // which may mean a change to the line range too $lineNumber.text(inlineInfo.editor.getFirstVisibleLine() + 1); }); // If Document's file is deleted, or Editor loses sync with Document, just close $(inlineInfo.editor).on("lostContent", function () { // Note: this closes the entire inline widget if any one Editor loses sync. This seems // better than leaving it open but suddenly removing one rule from the result list. self.close(); }); // set dirty indicator state _showDirtyIndicator($dirtyIndicatorDiv, doc.isDirty); }; /** * @param {Editor} hostEditor */ InlineTextEditor.prototype.load = function (hostEditor) { this.hostEditor = hostEditor; // TODO: incomplete impelementation. It's not clear yet if InlineTextEditor // will fuction as an abstract class or as generic inline editor implementation // that just shows a range of text. See CSSInlineEditor.css for an implementation of load() }; /** * Called when the editor containing the inline is made visible. */ InlineTextEditor.prototype.onParentShown = function () { // We need to call this explicitly whenever the host editor is reshown, since // we don't actually resize the inline editor while its host is invisible (see // isFullyVisible() check in sizeInlineWidgetToContents()). this.sizeInlineWidgetToContents(true); }; InlineTextEditor.prototype._editorHasFocus = function () { return this.editors.some(function (editor) { return editor.hasFocus(); }); }; // consolidate all dirty document updates $(DocumentManager).on("dirtyFlagChange", _dirtyFlagChangeHandler); exports.InlineTextEditor = InlineTextEditor; }); CodeMirror.defineMode("xml", function(config, parserConfig) { var indentUnit = config.indentUnit; var Kludges = parserConfig.htmlMode ? { autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, 'track': true, 'wbr': true}, implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, 'th': true, 'tr': true}, contextGrabbers: { 'dd': {'dd': true, 'dt': true}, 'dt': {'dd': true, 'dt': true}, 'li': {'li': true}, 'option': {'option': true, 'optgroup': true}, 'optgroup': {'optgroup': true}, 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, 'rp': {'rp': true, 'rt': true}, 'rt': {'rp': true, 'rt': true}, 'tbody': {'tbody': true, 'tfoot': true}, 'td': {'td': true, 'th': true}, 'tfoot': {'tbody': true}, 'th': {'td': true, 'th': true}, 'thead': {'tbody': true, 'tfoot': true}, 'tr': {'tr': true} }, doNotIndent: {"pre": true}, allowUnquoted: true, allowMissing: false } : { autoSelfClosers: {}, implicitlyClosed: {}, contextGrabbers: {}, doNotIndent: {}, allowUnquoted: false, allowMissing: false }; var alignCDATA = parserConfig.alignCDATA; // Return variables for tokenizers var tagName, type; function inText(stream, state) { function chain(parser) { state.tokenize = parser; return parser(stream, state); } var ch = stream.next(); if (ch == "<") { if (stream.eat("!")) { if (stream.eat("[")) { if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); else return null; } else if (stream.match("--")) return chain(inBlock("comment", "-->")); else if (stream.match("DOCTYPE", true, true)) { stream.eatWhile(/[\w\._\-]/); return chain(doctype(1)); } else return null; } else if (stream.eat("?")) { stream.eatWhile(/[\w\._\-]/); state.tokenize = inBlock("meta", "?>"); return "meta"; } else { type = stream.eat("/") ? "closeTag" : "openTag"; stream.eatSpace(); tagName = ""; var c; while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; state.tokenize = inTag; return "tag"; } } else if (ch == "&") { var ok; if (stream.eat("#")) { if (stream.eat("x")) { ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); } else { ok = stream.eatWhile(/[\d]/) && stream.eat(";"); } } else { ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); } return ok ? "atom" : "error"; } else { stream.eatWhile(/[^&<]/); return null; } } function inTag(stream, state) { var ch = stream.next(); if (ch == ">" || (ch == "/" && stream.eat(">"))) { state.tokenize = inText; type = ch == ">" ? "endTag" : "selfcloseTag"; return "tag"; } else if (ch == "=") { type = "equals"; return null; } else if (/[\'\"]/.test(ch)) { state.tokenize = inAttribute(ch); return state.tokenize(stream, state); } else { stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/); return "word"; } } function inAttribute(quote) { return function(stream, state) { while (!stream.eol()) { if (stream.next() == quote) { state.tokenize = inTag; break; } } return "string"; }; } function inBlock(style, terminator) { return function(stream, state) { while (!stream.eol()) { if (stream.match(terminator)) { state.tokenize = inText; break; } stream.next(); } return style; }; } function doctype(depth) { return function(stream, state) { var ch; while ((ch = stream.next()) != null) { if (ch == "<") { state.tokenize = doctype(depth + 1); return state.tokenize(stream, state); } else if (ch == ">") { if (depth == 1) { state.tokenize = inText; break; } else { state.tokenize = doctype(depth - 1); return state.tokenize(stream, state); } } } return "meta"; }; } var curState, setStyle; function pass() { for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]); } function cont() { pass.apply(null, arguments); return true; } function pushContext(tagName, startOfLine) { var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent); curState.context = { prev: curState.context, tagName: tagName, indent: curState.indented, startOfLine: startOfLine, noIndent: noIndent }; } function popContext() { if (curState.context) curState.context = curState.context.prev; } function element(type) { if (type == "openTag") { curState.tagName = tagName; return cont(attributes, endtag(curState.startOfLine)); } else if (type == "closeTag") { var err = false; if (curState.context) { if (curState.context.tagName != tagName) { if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) { popContext(); } err = !curState.context || curState.context.tagName != tagName; } } else { err = true; } if (err) setStyle = "error"; return cont(endclosetag(err)); } return cont(); } function endtag(startOfLine) { return function(type) { if (type == "selfcloseTag" || (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(curState.tagName.toLowerCase()))) { maybePopContext(curState.tagName.toLowerCase()); return cont(); } if (type == "endTag") { maybePopContext(curState.tagName.toLowerCase()); pushContext(curState.tagName, startOfLine); return cont(); } return cont(); }; } function endclosetag(err) { return function(type) { if (err) setStyle = "error"; if (type == "endTag") { popContext(); return cont(); } setStyle = "error"; return cont(arguments.callee); } } function maybePopContext(nextTagName) { var parentTagName; while (true) { if (!curState.context) { return; } parentTagName = curState.context.tagName.toLowerCase(); if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { return; } popContext(); } } function attributes(type) { if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);} if (type == "endTag" || type == "selfcloseTag") return pass(); setStyle = "error"; return cont(attributes); } function attribute(type) { if (type == "equals") return cont(attvalue, attributes); if (!Kludges.allowMissing) setStyle = "error"; return (type == "endTag" || type == "selfcloseTag") ? pass() : cont(); } function attvalue(type) { if (type == "string") return cont(attvaluemaybe); if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();} setStyle = "error"; return (type == "endTag" || type == "selfCloseTag") ? pass() : cont(); } function attvaluemaybe(type) { if (type == "string") return cont(attvaluemaybe); else return pass(); } return { startState: function() { return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, context: null}; }, token: function(stream, state) { if (stream.sol()) { state.startOfLine = true; state.indented = stream.indentation(); } if (stream.eatSpace()) return null; setStyle = type = tagName = null; var style = state.tokenize(stream, state); state.type = type; if ((style || type) && style != "comment") { curState = state; while (true) { var comb = state.cc.pop() || element; if (comb(type || style)) break; } } state.startOfLine = false; return setStyle || style; }, indent: function(state, textAfter, fullLine) { var context = state.context; if ((state.tokenize != inTag && state.tokenize != inText) || context && context.noIndent) return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; if (alignCDATA && /<!\[CDATA\[/.test(textAfter)) return 0; if (context && /^<\//.test(textAfter)) context = context.prev; while (context && !context.startOfLine) context = context.prev; if (context) return context.indent + indentUnit; else return 0; }, compareStates: function(a, b) { if (a.indented != b.indented || a.tokenize != b.tokenize) return false; for (var ca = a.context, cb = b.context; ; ca = ca.prev, cb = cb.prev) { if (!ca || !cb) return ca == cb; if (ca.tagName != cb.tagName) return false; } }, electricChars: "/" }; }); CodeMirror.defineMIME("application/xml", "xml"); if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); define("thirdparty/CodeMirror2/mode/xml/xml", function(){}); CodeMirror.defineMode("javascript", function(config, parserConfig) { var indentUnit = config.indentUnit; var jsonMode = parserConfig.json; // Tokenizer var keywords = function(){ function kw(type) {return {type: type, style: "keyword"};} var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); var operator = kw("operator"), atom = {type: "atom", style: "atom"}; return { "if": A, "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "var": kw("var"), "const": kw("var"), "let": kw("var"), "function": kw("function"), "catch": kw("catch"), "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), "in": operator, "typeof": operator, "instanceof": operator, "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom }; }(); var isOperatorChar = /[+\-*&%=<>!?|]/; function chain(stream, state, f) { state.tokenize = f; return f(stream, state); } function nextUntilUnescaped(stream, end) { var escaped = false, next; while ((next = stream.next()) != null) { if (next == end && !escaped) return false; escaped = !escaped && next == "\\"; } return escaped; } // Used as scratch variables to communicate multiple values without // consing up tons of objects. var type, content; function ret(tp, style, cont) { type = tp; content = cont; return style; } function jsTokenBase(stream, state) { var ch = stream.next(); if (ch == '"' || ch == "'") return chain(stream, state, jsTokenString(ch)); else if (/[\[\]{}\(\),;\:\.]/.test(ch)) return ret(ch); else if (ch == "0" && stream.eat(/x/i)) { stream.eatWhile(/[\da-f]/i); return ret("number", "number"); } else if (/\d/.test(ch) || ch == "-" && stream.eat(/\d/)) { stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); return ret("number", "number"); } else if (ch == "/") { if (stream.eat("*")) { return chain(stream, state, jsTokenComment); } else if (stream.eat("/")) { stream.skipToEnd(); return ret("comment", "comment"); } else if (state.reAllowed) { nextUntilUnescaped(stream, "/"); stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla return ret("regexp", "string-2"); } else { stream.eatWhile(isOperatorChar); return ret("operator", null, stream.current()); } } else if (ch == "#") { stream.skipToEnd(); return ret("error", "error"); } else if (isOperatorChar.test(ch)) { stream.eatWhile(isOperatorChar); return ret("operator", null, stream.current()); } else { stream.eatWhile(/[\w\$_]/); var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; return (known && state.kwAllowed) ? ret(known.type, known.style, word) : ret("variable", "variable", word); } } function jsTokenString(quote) { return function(stream, state) { if (!nextUntilUnescaped(stream, quote)) state.tokenize = jsTokenBase; return ret("string", "string"); }; } function jsTokenComment(stream, state) { var maybeEnd = false, ch; while (ch = stream.next()) { if (ch == "/" && maybeEnd) { state.tokenize = jsTokenBase; break; } maybeEnd = (ch == "*"); } return ret("comment", "comment"); } // Parser var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true}; function JSLexical(indented, column, type, align, prev, info) { this.indented = indented; this.column = column; this.type = type; this.prev = prev; this.info = info; if (align != null) this.align = align; } function inScope(state, varname) { for (var v = state.localVars; v; v = v.next) if (v.name == varname) return true; } function parseJS(state, style, type, content, stream) { var cc = state.cc; // Communicate our context to the combinators. // (Less wasteful than consing up a hundred closures on every call.) cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; if (!state.lexical.hasOwnProperty("align")) state.lexical.align = true; while(true) { var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; if (combinator(type, content)) { while(cc.length && cc[cc.length - 1].lex) cc.pop()(); if (cx.marked) return cx.marked; if (type == "variable" && inScope(state, content)) return "variable-2"; return style; } } } // Combinator utils var cx = {state: null, column: null, marked: null, cc: null}; function pass() { for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); } function cont() { pass.apply(null, arguments); return true; } function register(varname) { var state = cx.state; if (state.context) { cx.marked = "def"; for (var v = state.localVars; v; v = v.next) if (v.name == varname) return; state.localVars = {name: varname, next: state.localVars}; } } // Combinators var defaultVars = {name: "this", next: {name: "arguments"}}; function pushcontext() { if (!cx.state.context) cx.state.localVars = defaultVars; cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; } function popcontext() { cx.state.localVars = cx.state.context.vars; cx.state.context = cx.state.context.prev; } function pushlex(type, info) { var result = function() { var state = cx.state; state.lexical = new JSLexical(state.indented, cx.stream.column(), type, null, state.lexical, info) }; result.lex = true; return result; } function poplex() { var state = cx.state; if (state.lexical.prev) { if (state.lexical.type == ")") state.indented = state.lexical.indented; state.lexical = state.lexical.prev; } } poplex.lex = true; function expect(wanted) { return function expecting(type) { if (type == wanted) return cont(); else if (wanted == ";") return pass(); else return cont(arguments.callee); }; } function statement(type) { if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex); if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); if (type == "keyword b") return cont(pushlex("form"), statement, poplex); if (type == "{") return cont(pushlex("}"), block, poplex); if (type == ";") return cont(); if (type == "function") return cont(functiondef); if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), poplex, statement, poplex); if (type == "variable") return cont(pushlex("stat"), maybelabel); if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), block, poplex, poplex); if (type == "case") return cont(expression, expect(":")); if (type == "default") return cont(expect(":")); if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), statement, poplex, popcontext); return pass(pushlex("stat"), expression, expect(";"), poplex); } function expression(type) { if (atomicTypes.hasOwnProperty(type)) return cont(maybeoperator); if (type == "function") return cont(functiondef); if (type == "keyword c") return cont(maybeexpression); if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeoperator); if (type == "operator") return cont(expression); if (type == "[") return cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator); if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator); return cont(); } function maybeexpression(type) { if (type.match(/[;\}\)\],]/)) return pass(); return pass(expression); } function maybeoperator(type, value) { if (type == "operator" && /\+\+|--/.test(value)) return cont(maybeoperator); if (type == "operator" || type == ":") return cont(expression); if (type == ";") return; if (type == "(") return cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator); if (type == ".") return cont(property, maybeoperator); if (type == "[") return cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator); } function maybelabel(type) { if (type == ":") return cont(poplex, statement); return pass(maybeoperator, expect(";"), poplex); } function property(type) { if (type == "variable") {cx.marked = "property"; return cont();} } function objprop(type) { if (type == "variable") cx.marked = "property"; if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expression); } function commasep(what, end) { function proceed(type) { if (type == ",") return cont(what, proceed); if (type == end) return cont(); return cont(expect(end)); } return function commaSeparated(type) { if (type == end) return cont(); else return pass(what, proceed); }; } function block(type) { if (type == "}") return cont(); return pass(statement, block); } function vardef1(type, value) { if (type == "variable"){register(value); return cont(vardef2);} return cont(); } function vardef2(type, value) { if (value == "=") return cont(expression, vardef2); if (type == ",") return cont(vardef1); } function forspec1(type) { if (type == "var") return cont(vardef1, forspec2); if (type == ";") return pass(forspec2); if (type == "variable") return cont(formaybein); return pass(forspec2); } function formaybein(type, value) { if (value == "in") return cont(expression); return cont(maybeoperator, forspec2); } function forspec2(type, value) { if (type == ";") return cont(forspec3); if (value == "in") return cont(expression); return cont(expression, expect(";"), forspec3); } function forspec3(type) { if (type != ")") cont(expression); } function functiondef(type, value) { if (type == "variable") {register(value); return cont(functiondef);} if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext); } function funarg(type, value) { if (type == "variable") {register(value); return cont();} } // Interface return { startState: function(basecolumn) { return { tokenize: jsTokenBase, reAllowed: true, kwAllowed: true, cc: [], lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), localVars: parserConfig.localVars, context: parserConfig.localVars && {vars: parserConfig.localVars}, indented: 0 }; }, token: function(stream, state) { if (stream.sol()) { if (!state.lexical.hasOwnProperty("align")) state.lexical.align = false; state.indented = stream.indentation(); } if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); if (type == "comment") return style; state.reAllowed = !!(type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/)); state.kwAllowed = type != '.'; return parseJS(state, style, type, content, stream); }, indent: function(state, textAfter) { if (state.tokenize != jsTokenBase) return 0; var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; var type = lexical.type, closing = firstChar == type; if (type == "vardef") return lexical.indented + 4; else if (type == "form" && firstChar == "{") return lexical.indented; else if (type == "stat" || type == "form") return lexical.indented + indentUnit; else if (lexical.info == "switch" && !closing) return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); else if (lexical.align) return lexical.column + (closing ? 0 : 1); else return lexical.indented + (closing ? 0 : indentUnit); }, electricChars: ":{}" }; }); CodeMirror.defineMIME("text/javascript", "javascript"); CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); define("thirdparty/CodeMirror2/mode/javascript/javascript", function(){}); CodeMirror.defineMode("css", function(config) { var indentUnit = config.indentUnit, type; function ret(style, tp) {type = tp; return style;} function tokenBase(stream, state) { var ch = stream.next(); if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("meta", stream.current());} else if (ch == "/" && stream.eat("*")) { state.tokenize = tokenCComment; return tokenCComment(stream, state); } else if (ch == "<" && stream.eat("!")) { state.tokenize = tokenSGMLComment; return tokenSGMLComment(stream, state); } else if (ch == "=") ret(null, "compare"); else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare"); else if (ch == "\"" || ch == "'") { state.tokenize = tokenString(ch); return state.tokenize(stream, state); } else if (ch == "#") { stream.eatWhile(/[\w\\\-]/); return ret("atom", "hash"); } else if (ch == "!") { stream.match(/^\s*\w*/); return ret("keyword", "important"); } else if (/\d/.test(ch)) { stream.eatWhile(/[\w.%]/); return ret("number", "unit"); } else if (/[,.+>*\/]/.test(ch)) { return ret(null, "select-op"); } else if (/[;{}:\[\]]/.test(ch)) { return ret(null, ch); } else { stream.eatWhile(/[\w\\\-]/); return ret("variable", "variable"); } } function tokenCComment(stream, state) { var maybeEnd = false, ch; while ((ch = stream.next()) != null) { if (maybeEnd && ch == "/") { state.tokenize = tokenBase; break; } maybeEnd = (ch == "*"); } return ret("comment", "comment"); } function tokenSGMLComment(stream, state) { var dashes = 0, ch; while ((ch = stream.next()) != null) { if (dashes >= 2 && ch == ">") { state.tokenize = tokenBase; break; } dashes = (ch == "-") ? dashes + 1 : 0; } return ret("comment", "comment"); } function tokenString(quote) { return function(stream, state) { var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && !escaped) break; escaped = !escaped && ch == "\\"; } if (!escaped) state.tokenize = tokenBase; return ret("string", "string"); }; } return { startState: function(base) { return {tokenize: tokenBase, baseIndent: base || 0, stack: []}; }, token: function(stream, state) { if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); var context = state.stack[state.stack.length-1]; if (type == "hash" && context != "rule") style = "string-2"; else if (style == "variable") { if (context == "rule") style = "number"; else if (!context || context == "@media{") style = "tag"; } if (context == "rule" && /^[\{\};]$/.test(type)) state.stack.pop(); if (type == "{") { if (context == "@media") state.stack[state.stack.length-1] = "@media{"; else state.stack.push("{"); } else if (type == "}") state.stack.pop(); else if (type == "@media") state.stack.push("@media"); else if (context == "{" && type != "comment") state.stack.push("rule"); return style; }, indent: function(state, textAfter) { var n = state.stack.length; if (/^\}/.test(textAfter)) n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1; return state.baseIndent + n * indentUnit; }, electricChars: "}" }; }); CodeMirror.defineMIME("text/css", "css"); define("thirdparty/CodeMirror2/mode/css/css", function(){}); /* LESS mode - http://www.lesscss.org/ Ported to CodeMirror by Peter Kroon */ CodeMirror.defineMode("less", function(config) { var indentUnit = config.indentUnit, type; function ret(style, tp) {type = tp; return style;} //html5 tags var tags = ["a","abbr","acronym","address","applet","area","article","aside","audio","b","base","basefont","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","command","datalist","dd","del","details","dfn","dir","div","dl","dt","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","keygen","kbd","label","legend","li","link","map","mark","menu","meta","meter","nav","noframes","noscript","object","ol","optgroup","option","output","p","param","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strike","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","tt","u","ul","var","video","wbr"]; function inTagsArray(val){ for(var i=0; i<tags.length; i++){ if(val === tags[i]){ return true; } } } function tokenBase(stream, state) { var ch = stream.next(); if (ch == "@") {stream.eatWhile(/[\w\-]/); return ret("meta", stream.current());} else if (ch == "/" && stream.eat("*")) { state.tokenize = tokenCComment; return tokenCComment(stream, state); } else if (ch == "<" && stream.eat("!")) { state.tokenize = tokenSGMLComment; return tokenSGMLComment(stream, state); } else if (ch == "=") ret(null, "compare"); else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare"); else if (ch == "\"" || ch == "'") { state.tokenize = tokenString(ch); return state.tokenize(stream, state); } else if (ch == "/") { // lesscss e.g.: .png will not be parsed as a class if(stream.eat("/")){ state.tokenize = tokenSComment return tokenSComment(stream, state); }else{ stream.eatWhile(/[\a-zA-Z0-9\-_.\s]/); if(/\/|\)|#/.test(stream.peek() || stream.eol() || (stream.eatSpace() && stream.peek() == ")")))return ret("string", "string");//let url(/images/logo.png) without quotes return as string return ret("number", "unit"); } } else if (ch == "!") { stream.match(/^\s*\w*/); return ret("keyword", "important"); } else if (/\d/.test(ch)) { stream.eatWhile(/[\w.%]/); return ret("number", "unit"); } else if (/[,+<>*\/]/.test(ch)) {//removed . dot character original was [,.+>*\/] return ret(null, "select-op"); } else if (/[;{}:\[\]()]/.test(ch)) { //added () char for lesscss original was [;{}:\[\]] if(ch == ":"){ stream.eatWhile(/[active|hover|link|visited]/); if( stream.current().match(/active|hover|link|visited/)){ return ret("tag", "tag"); }else{ return ret(null, ch); } }else{ return ret(null, ch); } } else if (ch == ".") { // lesscss stream.eatWhile(/[\a-zA-Z0-9\-_]/); return ret("tag", "tag"); } else if (ch == "#") { // lesscss //we don't eat white-space, we want the hex color and or id only stream.eatWhile(/[A-Za-z0-9]/); //check if there is a proper hex color length e.g. #eee || #eeeEEE if(stream.current().length ===4 || stream.current().length ===7){ if(stream.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,false) != null){//is there a valid hex color value present in the current stream //when not a valid hex value, parse as id if(stream.current().substring(1) != stream.current().match(/[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/,false))return ret("atom", "tag"); //eat white-space stream.eatSpace(); //when hex value declaration doesn't end with [;,] but is does with a slash/cc comment treat it as an id, just like the other hex values that don't end with[;,] if( /[\/<>.(){!$%^&*_\-\\?=+\|#'~`]/.test(stream.peek()) )return ret("atom", "tag"); //#time { color: #aaa } else if(stream.peek() == "}" )return ret("number", "unit"); //we have a valid hex color value, parse as id whenever an element/class is defined after the hex(id) value e.g. #eee aaa || #eee .aaa else if( /[a-zA-Z\\]/.test(stream.peek()) )return ret("atom", "tag"); //when a hex value is on the end of a line, parse as id else if(stream.eol())return ret("atom", "tag"); //default else return ret("number", "unit"); }else{//when not a valid hexvalue in the current stream e.g. #footer stream.eatWhile(/[\w\\\-]/); return ret("atom", "tag"); } }else{ stream.eatWhile(/[\w\\\-]/); return ret("atom", "tag"); } } else if (ch == "&") { stream.eatWhile(/[\w\-]/); return ret(null, ch); } else { stream.eatWhile(/[\w\\\-_%.{]/); if(stream.current().match(/http|https/) != null){ stream.eatWhile(/[\w\\\-_%.{:\/]/); return ret("string", "string"); }else if(stream.peek() == "<" || stream.peek() == ">"){ return ret("tag", "tag"); }else if( stream.peek().match(/\(/) != null ){// lessc return ret(null, ch); }else if (stream.peek() == "/" && state.stack[state.stack.length-1] != undefined){ // url(dir/center/image.png) return ret("string", "string"); }else if( stream.current().match(/\-\d|\-.\d/) ){ // lesscss match e.g.: -5px -0.4 etc... only colorize the minus sign //stream.backUp(stream.current().length-1); //commment out these 2 comment if you want the minus sign to be parsed as null -500px //return ret(null, ch); return ret("number", "unit"); }else if( inTagsArray(stream.current()) ){ // lesscss match html tags return ret("tag", "tag"); }else if( /\/|[\s\)]/.test(stream.peek() || stream.eol() || (stream.eatSpace() && stream.peek() == "/")) && stream.current().indexOf(".") !== -1){ if(stream.current().substring(stream.current().length-1,stream.current().length) == "{"){ stream.backUp(1); return ret("tag", "tag"); }//end if if( (stream.eatSpace() && stream.peek().match(/[{<>.a-zA-Z]/) != null) || stream.eol() )return ret("tag", "tag");//e.g. button.icon-plus return ret("string", "string");//let url(/images/logo.png) without quotes return as string }else if( stream.eol() ){ if(stream.current().substring(stream.current().length-1,stream.current().length) == "{")stream.backUp(1); return ret("tag", "tag"); }else{ return ret("variable", "variable"); } } } function tokenSComment(stream, state) {// SComment = Slash comment stream.skipToEnd(); state.tokenize = tokenBase; return ret("comment", "comment"); } function tokenCComment(stream, state) { var maybeEnd = false, ch; while ((ch = stream.next()) != null) { if (maybeEnd && ch == "/") { state.tokenize = tokenBase; break; } maybeEnd = (ch == "*"); } return ret("comment", "comment"); } function tokenSGMLComment(stream, state) { var dashes = 0, ch; while ((ch = stream.next()) != null) { if (dashes >= 2 && ch == ">") { state.tokenize = tokenBase; break; } dashes = (ch == "-") ? dashes + 1 : 0; } return ret("comment", "comment"); } function tokenString(quote) { return function(stream, state) { var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && !escaped) break; escaped = !escaped && ch == "\\"; } if (!escaped) state.tokenize = tokenBase; return ret("string", "string"); }; } return { startState: function(base) { return {tokenize: tokenBase, baseIndent: base || 0, stack: []}; }, token: function(stream, state) { if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); var context = state.stack[state.stack.length-1]; if (type == "hash" && context == "rule") style = "atom"; else if (style == "variable") { if (context == "rule") style = null; //"tag" else if (!context || context == "@media{"){ style = stream.current() == "when" ? "variable" : stream.string.match(/#/g) != undefined ? null : /[\s,|\s\)]/.test(stream.peek()) ? "tag" : null; } } if (context == "rule" && /^[\{\};]$/.test(type)) state.stack.pop(); if (type == "{") { if (context == "@media") state.stack[state.stack.length-1] = "@media{"; else state.stack.push("{"); } else if (type == "}") state.stack.pop(); else if (type == "@media") state.stack.push("@media"); else if (context == "{" && type != "comment") state.stack.push("rule"); return style; }, indent: function(state, textAfter) { var n = state.stack.length; if (/^\}/.test(textAfter)) n -= state.stack[state.stack.length-1] == "rule" ? 2 : 1; return state.baseIndent + n * indentUnit; }, electricChars: "}" }; }); CodeMirror.defineMIME("text/x-less", "less"); if (!CodeMirror.mimeModes.hasOwnProperty("text/css")) CodeMirror.defineMIME("text/css", "less"); define("thirdparty/CodeMirror2/mode/less/less", function(){}); CodeMirror.defineMode("htmlmixed", function(config, parserConfig) { var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); var unknownScriptMode = CodeMirror.getMode(config, "text/plain"); var jsMode = CodeMirror.getMode(config, "javascript"); var cssMode = CodeMirror.getMode(config, "css"); function html(stream, state) { var style = htmlMode.token(stream, state.htmlState); if (style == "tag" && stream.current() == ">" && state.htmlState.context) { if (/^script$/i.test(state.htmlState.context.tagName)) { // Script block: mode to change to depends on type attribute var scriptType = stream.string.match(/type\s*=\s*["'](.+)["']/i); scriptType = scriptType && scriptType[1]; if (!scriptType || scriptType.match(/(text|application)\/(java|ecma)script/i)) { state.token = javascript; state.localState = jsMode.startState(htmlMode.indent(state.htmlState, "")); state.mode = "javascript"; } else if (scriptType.match(/\/x-handlebars-template/i) || scriptType.match(/\/x-mustache/i)) { // Handlebars or Mustache template: leave it in HTML mode } else { state.token = unknownScript; state.localState = null; state.mode = ""; } } else if (/^style$/i.test(state.htmlState.context.tagName)) { state.token = css; state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); state.mode = "css"; } } return style; } function maybeBackup(stream, pat, style) { var cur = stream.current(); var close = cur.search(pat); if (close > -1) stream.backUp(cur.length - close); return style; } function unknownScript(stream, state) { if (stream.match(/^<\/\s*script\s*>/i, false)) { state.token = html; state.localState = null; state.mode = "html"; return html(stream, state); } return maybeBackup(stream, /<\/\s*script\s*>/, unknownScriptMode.token(stream, state.localState)); } function javascript(stream, state) { if (stream.match(/^<\/\s*script\s*>/i, false)) { state.token = html; state.localState = null; state.mode = "html"; return html(stream, state); } return maybeBackup(stream, /<\/\s*script\s*>/, jsMode.token(stream, state.localState)); } function css(stream, state) { if (stream.match(/^<\/\s*style\s*>/i, false)) { state.token = html; state.localState = null; state.mode = "html"; return html(stream, state); } return maybeBackup(stream, /<\/\s*style\s*>/, cssMode.token(stream, state.localState)); } return { startState: function() { var state = htmlMode.startState(); return {token: html, localState: null, mode: "html", htmlState: state}; }, copyState: function(state) { if (state.localState) var local = CodeMirror.copyState(state.token == css ? cssMode : jsMode, state.localState); return {token: state.token, localState: local, mode: state.mode, htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; }, token: function(stream, state) { return state.token(stream, state); }, indent: function(state, textAfter) { if (state.token == html || /^\s*<\//.test(textAfter)) return htmlMode.indent(state.htmlState, textAfter); else if (state.token == javascript) return jsMode.indent(state.localState, textAfter); else if (state.token == css) return cssMode.indent(state.localState, textAfter); else // unknownScriptMode return 0; }, compareStates: function(a, b) { if (a.mode != b.mode) return false; if (a.localState) return CodeMirror.Pass; return htmlMode.compareStates(a.htmlState, b.htmlState); }, electricChars: "/{}:" } }, "xml", "javascript", "css"); CodeMirror.defineMIME("text/html", "htmlmixed"); define("thirdparty/CodeMirror2/mode/htmlmixed/htmlmixed", function(){}); CodeMirror.defineMode("clike", function(config, parserConfig) { var indentUnit = config.indentUnit, keywords = parserConfig.keywords || {}, builtin = parserConfig.builtin || {}, blockKeywords = parserConfig.blockKeywords || {}, atoms = parserConfig.atoms || {}, hooks = parserConfig.hooks || {}, multiLineStrings = parserConfig.multiLineStrings; var isOperatorChar = /[+\-*&%=<>!?|\/]/; var curPunc; function tokenBase(stream, state) { var ch = stream.next(); if (hooks[ch]) { var result = hooks[ch](stream, state); if (result !== false) return result; } if (ch == '"' || ch == "'") { state.tokenize = tokenString(ch); return state.tokenize(stream, state); } if (/[\[\]{}\(\),;\:\.]/.test(ch)) { curPunc = ch; return null; } if (/\d/.test(ch)) { stream.eatWhile(/[\w\.]/); return "number"; } if (ch == "/") { if (stream.eat("*")) { state.tokenize = tokenComment; return tokenComment(stream, state); } if (stream.eat("/")) { stream.skipToEnd(); return "comment"; } } if (isOperatorChar.test(ch)) { stream.eatWhile(isOperatorChar); return "operator"; } stream.eatWhile(/[\w\$_]/); var cur = stream.current(); if (keywords.propertyIsEnumerable(cur)) { if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; return "keyword"; } if (builtin.propertyIsEnumerable(cur)) { if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; return "builtin"; } if (atoms.propertyIsEnumerable(cur)) return "atom"; return "word"; } function tokenString(quote) { return function(stream, state) { var escaped = false, next, end = false; while ((next = stream.next()) != null) { if (next == quote && !escaped) {end = true; break;} escaped = !escaped && next == "\\"; } if (end || !(escaped || multiLineStrings)) state.tokenize = null; return "string"; }; } function tokenComment(stream, state) { var maybeEnd = false, ch; while (ch = stream.next()) { if (ch == "/" && maybeEnd) { state.tokenize = null; break; } maybeEnd = (ch == "*"); } return "comment"; } function Context(indented, column, type, align, prev) { this.indented = indented; this.column = column; this.type = type; this.align = align; this.prev = prev; } function pushContext(state, col, type) { return state.context = new Context(state.indented, col, type, null, state.context); } function popContext(state) { var t = state.context.type; if (t == ")" || t == "]" || t == "}") state.indented = state.context.indented; return state.context = state.context.prev; } // Interface return { startState: function(basecolumn) { return { tokenize: null, context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), indented: 0, startOfLine: true }; }, token: function(stream, state) { var ctx = state.context; if (stream.sol()) { if (ctx.align == null) ctx.align = false; state.indented = stream.indentation(); state.startOfLine = true; } if (stream.eatSpace()) return null; curPunc = null; var style = (state.tokenize || tokenBase)(stream, state); if (style == "comment" || style == "meta") return style; if (ctx.align == null) ctx.align = true; if ((curPunc == ";" || curPunc == ":") && ctx.type == "statement") popContext(state); else if (curPunc == "{") pushContext(state, stream.column(), "}"); else if (curPunc == "[") pushContext(state, stream.column(), "]"); else if (curPunc == "(") pushContext(state, stream.column(), ")"); else if (curPunc == "}") { while (ctx.type == "statement") ctx = popContext(state); if (ctx.type == "}") ctx = popContext(state); while (ctx.type == "statement") ctx = popContext(state); } else if (curPunc == ctx.type) popContext(state); else if (ctx.type == "}" || ctx.type == "top" || (ctx.type == "statement" && curPunc == "newstatement")) pushContext(state, stream.column(), "statement"); state.startOfLine = false; return style; }, indent: function(state, textAfter) { if (state.tokenize != tokenBase && state.tokenize != null) return 0; var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; var closing = firstChar == ctx.type; if (ctx.type == "statement") return ctx.indented + (firstChar == "{" ? 0 : indentUnit); else if (ctx.align) return ctx.column + (closing ? 0 : 1); else return ctx.indented + (closing ? 0 : indentUnit); }, electricChars: "{}" }; }); (function() { function words(str) { var obj = {}, words = str.split(" "); for (var i = 0; i < words.length; ++i) obj[words[i]] = true; return obj; } var cKeywords = "auto if break int case long char register continue return default short do sizeof " + "double static else struct entry switch extern typedef float union for unsigned " + "goto while enum void const signed volatile"; function cppHook(stream, state) { if (!state.startOfLine) return false; stream.skipToEnd(); return "meta"; } // C#-style strings where "" escapes a quote. function tokenAtString(stream, state) { var next; while ((next = stream.next()) != null) { if (next == '"' && !stream.eat('"')) { state.tokenize = null; break; } } return "string"; } CodeMirror.defineMIME("text/x-csrc", { name: "clike", keywords: words(cKeywords), blockKeywords: words("case do else for if switch while struct"), atoms: words("null"), hooks: {"#": cppHook} }); CodeMirror.defineMIME("text/x-c++src", { name: "clike", keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try bool explicit new " + "static_cast typeid catch operator template typename class friend private " + "this using const_cast inline public throw virtual delete mutable protected " + "wchar_t"), blockKeywords: words("catch class do else finally for if struct switch try while"), atoms: words("true false null"), hooks: {"#": cppHook} }); CodeMirror.defineMIME("text/x-java", { name: "clike", keywords: words("abstract assert boolean break byte case catch char class const continue default " + "do double else enum extends final finally float for goto if implements import " + "instanceof int interface long native new package private protected public " + "return short static strictfp super switch synchronized this throw throws transient " + "try void volatile while"), blockKeywords: words("catch class do else finally for if switch try while"), atoms: words("true false null"), hooks: { "@": function(stream, state) { stream.eatWhile(/[\w\$_]/); return "meta"; } } }); CodeMirror.defineMIME("text/x-csharp", { name: "clike", keywords: words("abstract as base break case catch checked class const continue" + " default delegate do else enum event explicit extern finally fixed for" + " foreach goto if implicit in interface internal is lock namespace new" + " operator out override params private protected public readonly ref return sealed" + " sizeof stackalloc static struct switch this throw try typeof unchecked" + " unsafe using virtual void volatile while add alias ascending descending dynamic from get" + " global group into join let orderby partial remove select set value var yield"), blockKeywords: words("catch class do else finally for foreach if struct switch try while"), builtin: words("Boolean Byte Char DateTime DateTimeOffset Decimal Double" + " Guid Int16 Int32 Int64 Object SByte Single String TimeSpan UInt16 UInt32" + " UInt64 bool byte char decimal double short int long object" + " sbyte float string ushort uint ulong"), atoms: words("true false null"), hooks: { "@": function(stream, state) { if (stream.eat('"')) { state.tokenize = tokenAtString; return tokenAtString(stream, state); } stream.eatWhile(/[\w\$_]/); return "meta"; } } }); CodeMirror.defineMIME("text/x-scala", { name: "clike", keywords: words( /* scala */ "abstract case catch class def do else extends false final finally for forSome if " + "implicit import lazy match new null object override package private protected return " + "sealed super this throw trait try trye type val var while with yield _ : = => <- <: " + "<% >: # @ " + /* package scala */ "assert assume require print println printf readLine readBoolean readByte readShort " + "readChar readInt readLong readFloat readDouble " + "AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either " + "Enumeration Equiv Error Exception Fractional Function IndexedSeq Integral Iterable " + "Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering " + "Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder " + "StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector :: #:: " + /* package java.lang */ "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void" ), blockKeywords: words("catch class do else finally for forSome if match switch try while"), atoms: words("true false null"), hooks: { "@": function(stream, state) { stream.eatWhile(/[\w\$_]/); return "meta"; } } }); }()); define("thirdparty/CodeMirror2/mode/clike/clike", function(){}); (function() { function keywords(str) { var obj = {}, words = str.split(" "); for (var i = 0; i < words.length; ++i) obj[words[i]] = true; return obj; } function heredoc(delim) { return function(stream, state) { if (stream.match(delim)) state.tokenize = null; else stream.skipToEnd(); return "string"; } } var phpConfig = { name: "clike", keywords: keywords("abstract and array as break case catch class clone const continue declare default " + "do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final " + "for foreach function global goto if implements interface instanceof namespace " + "new or private protected public static switch throw trait try use var while xor " + "die echo empty exit eval include include_once isset list require require_once return " + "print unset __halt_compiler self static parent"), blockKeywords: keywords("catch do else elseif for foreach if switch try while"), atoms: keywords("true false null TRUE FALSE NULL"), multiLineStrings: true, hooks: { "$": function(stream, state) { stream.eatWhile(/[\w\$_]/); return "variable-2"; }, "<": function(stream, state) { if (stream.match(/<</)) { stream.eatWhile(/[\w\.]/); state.tokenize = heredoc(stream.current().slice(3)); return state.tokenize(stream, state); } return false; }, "#": function(stream, state) { while (!stream.eol() && !stream.match("?>", false)) stream.next(); return "comment"; }, "/": function(stream, state) { if (stream.eat("/")) { while (!stream.eol() && !stream.match("?>", false)) stream.next(); return "comment"; } return false; } } }; CodeMirror.defineMode("php", function(config, parserConfig) { var htmlMode = CodeMirror.getMode(config, {name: "xml", htmlMode: true}); var jsMode = CodeMirror.getMode(config, "javascript"); var cssMode = CodeMirror.getMode(config, "css"); var phpMode = CodeMirror.getMode(config, phpConfig); function dispatch(stream, state) { // TODO open PHP inside text/css var isPHP = state.mode == "php"; if (stream.sol() && state.pending != '"') state.pending = null; if (state.curMode == htmlMode) { if (stream.match(/^<\?\w*/)) { state.curMode = phpMode; state.curState = state.php; state.curClose = "?>"; state.mode = "php"; return "meta"; } if (state.pending == '"') { while (!stream.eol() && stream.next() != '"') {} var style = "string"; } else if (state.pending && stream.pos < state.pending.end) { stream.pos = state.pending.end; var style = state.pending.style; } else { var style = htmlMode.token(stream, state.curState); } state.pending = null; var cur = stream.current(), openPHP = cur.search(/<\?/); if (openPHP != -1) { if (style == "string" && /\"$/.test(cur) && !/\?>/.test(cur)) state.pending = '"'; else state.pending = {end: stream.pos, style: style}; stream.backUp(cur.length - openPHP); } else if (style == "tag" && stream.current() == ">" && state.curState.context) { if (/^script$/i.test(state.curState.context.tagName)) { state.curMode = jsMode; state.curState = jsMode.startState(htmlMode.indent(state.curState, "")); state.curClose = /^<\/\s*script\s*>/i; state.mode = "javascript"; } else if (/^style$/i.test(state.curState.context.tagName)) { state.curMode = cssMode; state.curState = cssMode.startState(htmlMode.indent(state.curState, "")); state.curClose = /^<\/\s*style\s*>/i; state.mode = "css"; } } return style; } else if ((!isPHP || state.php.tokenize == null) && stream.match(state.curClose, isPHP)) { state.curMode = htmlMode; state.curState = state.html; state.curClose = null; state.mode = "html"; if (isPHP) return "meta"; else return dispatch(stream, state); } else { return state.curMode.token(stream, state.curState); } } return { startState: function() { var html = htmlMode.startState(); return {html: html, php: phpMode.startState(), curMode: parserConfig.startOpen ? phpMode : htmlMode, curState: parserConfig.startOpen ? phpMode.startState() : html, curClose: parserConfig.startOpen ? /^\?>/ : null, mode: parserConfig.startOpen ? "php" : "html", pending: null} }, copyState: function(state) { var html = state.html, htmlNew = CodeMirror.copyState(htmlMode, html), php = state.php, phpNew = CodeMirror.copyState(phpMode, php), cur; if (state.curState == html) cur = htmlNew; else if (state.curState == php) cur = phpNew; else cur = CodeMirror.copyState(state.curMode, state.curState); return {html: htmlNew, php: phpNew, curMode: state.curMode, curState: cur, curClose: state.curClose, mode: state.mode, pending: state.pending}; }, token: dispatch, indent: function(state, textAfter) { if ((state.curMode != phpMode && /^\s*<\//.test(textAfter)) || (state.curMode == phpMode && /^\?>/.test(textAfter))) return htmlMode.indent(state.html, textAfter); return state.curMode.indent(state.curState, textAfter); }, electricChars: "/{}:" } }, "xml", "clike", "javascript", "css"); CodeMirror.defineMIME("application/x-httpd-php", "php"); CodeMirror.defineMIME("application/x-httpd-php-open", {name: "php", startOpen: true}); CodeMirror.defineMIME("text/x-php", phpConfig); })(); define("thirdparty/CodeMirror2/mode/php/php", function(){}); /** * Link to the project's GitHub page: * https://github.com/pickhardt/coffeescript-codemirror-mode */ CodeMirror.defineMode('coffeescript', function(conf) { var ERRORCLASS = 'error'; function wordRegexp(words) { return new RegExp("^((" + words.join(")|(") + "))\\b"); } var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\?]"); var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]'); var doubleOperators = new RegExp("^((\->)|(\=>)|(\\+\\+)|(\\+\\=)|(\\-\\-)|(\\-\\=)|(\\*\\*)|(\\*\\=)|(\\/\\/)|(\\/\\=)|(==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//))"); var doubleDelimiters = new RegExp("^((\\.\\.)|(\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); var tripleDelimiters = new RegExp("^((\\.\\.\\.)|(//=)|(>>=)|(<<=)|(\\*\\*=))"); var identifiers = new RegExp("^[_A-Za-z$][_A-Za-z$0-9]*"); var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'isnt', 'in', 'instanceof', 'typeof']); var indentKeywords = ['for', 'while', 'loop', 'if', 'unless', 'else', 'switch', 'try', 'catch', 'finally', 'class']; var commonKeywords = ['break', 'by', 'continue', 'debugger', 'delete', 'do', 'in', 'of', 'new', 'return', 'then', 'this', 'throw', 'when', 'until']; var keywords = wordRegexp(indentKeywords.concat(commonKeywords)); indentKeywords = wordRegexp(indentKeywords); var stringPrefixes = new RegExp("^('{3}|\"{3}|['\"])"); var regexPrefixes = new RegExp("^(/{3}|/)"); var commonConstants = ['Infinity', 'NaN', 'undefined', 'null', 'true', 'false', 'on', 'off', 'yes', 'no']; var constants = wordRegexp(commonConstants); // Tokenizers function tokenBase(stream, state) { // Handle scope changes if (stream.sol()) { var scopeOffset = state.scopes[0].offset; if (stream.eatSpace()) { var lineOffset = stream.indentation(); if (lineOffset > scopeOffset) { return 'indent'; } else if (lineOffset < scopeOffset) { return 'dedent'; } return null; } else { if (scopeOffset > 0) { dedent(stream, state); } } } if (stream.eatSpace()) { return null; } var ch = stream.peek(); // Handle docco title comment (single line) if (stream.match("####")) { stream.skipToEnd(); return 'comment'; } // Handle multi line comments if (stream.match("###")) { state.tokenize = longComment; return state.tokenize(stream, state); } // Single line comment if (ch === '#') { stream.skipToEnd(); return 'comment'; } // Handle number literals if (stream.match(/^-?[0-9\.]/, false)) { var floatLiteral = false; // Floats if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) { floatLiteral = true; } if (stream.match(/^-?\d+\.\d*/)) { floatLiteral = true; } if (stream.match(/^-?\.\d+/)) { floatLiteral = true; } if (floatLiteral) { // prevent from getting extra . on 1.. if (stream.peek() == "."){ stream.backUp(1); } return 'number'; } // Integers var intLiteral = false; // Hex if (stream.match(/^-?0x[0-9a-f]+/i)) { intLiteral = true; } // Decimal if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) { intLiteral = true; } // Zero by itself with no other piece of number. if (stream.match(/^-?0(?![\dx])/i)) { intLiteral = true; } if (intLiteral) { return 'number'; } } // Handle strings if (stream.match(stringPrefixes)) { state.tokenize = tokenFactory(stream.current(), 'string'); return state.tokenize(stream, state); } // Handle regex literals if (stream.match(regexPrefixes)) { if (stream.current() != '/' || stream.match(/^.*\//, false)) { // prevent highlight of division state.tokenize = tokenFactory(stream.current(), 'string-2'); return state.tokenize(stream, state); } else { stream.backUp(1); } } // Handle operators and delimiters if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) { return 'punctuation'; } if (stream.match(doubleOperators) || stream.match(singleOperators) || stream.match(wordOperators)) { return 'operator'; } if (stream.match(singleDelimiters)) { return 'punctuation'; } if (stream.match(constants)) { return 'atom'; } if (stream.match(keywords)) { return 'keyword'; } if (stream.match(identifiers)) { return 'variable'; } // Handle non-detected items stream.next(); return ERRORCLASS; } function tokenFactory(delimiter, outclass) { var singleline = delimiter.length == 1; return function tokenString(stream, state) { while (!stream.eol()) { stream.eatWhile(/[^'"\/\\]/); if (stream.eat('\\')) { stream.next(); if (singleline && stream.eol()) { return outclass; } } else if (stream.match(delimiter)) { state.tokenize = tokenBase; return outclass; } else { stream.eat(/['"\/]/); } } if (singleline) { if (conf.mode.singleLineStringErrors) { outclass = ERRORCLASS } else { state.tokenize = tokenBase; } } return outclass; }; } function longComment(stream, state) { while (!stream.eol()) { stream.eatWhile(/[^#]/); if (stream.match("###")) { state.tokenize = tokenBase; break; } stream.eatWhile("#"); } return "comment" } function indent(stream, state, type) { type = type || 'coffee'; var indentUnit = 0; if (type === 'coffee') { for (var i = 0; i < state.scopes.length; i++) { if (state.scopes[i].type === 'coffee') { indentUnit = state.scopes[i].offset + conf.indentUnit; break; } } } else { indentUnit = stream.column() + stream.current().length; } state.scopes.unshift({ offset: indentUnit, type: type }); } function dedent(stream, state) { if (state.scopes.length == 1) return; if (state.scopes[0].type === 'coffee') { var _indent = stream.indentation(); var _indent_index = -1; for (var i = 0; i < state.scopes.length; ++i) { if (_indent === state.scopes[i].offset) { _indent_index = i; break; } } if (_indent_index === -1) { return true; } while (state.scopes[0].offset !== _indent) { state.scopes.shift(); } return false } else { state.scopes.shift(); return false; } } function tokenLexer(stream, state) { var style = state.tokenize(stream, state); var current = stream.current(); // Handle '.' connected identifiers if (current === '.') { style = state.tokenize(stream, state); current = stream.current(); if (style === 'variable') { return 'variable'; } else { return ERRORCLASS; } } // Handle properties if (current === '@') { stream.eat('@'); return 'keyword'; } // Handle scope changes. if (current === 'return') { state.dedent += 1; } if (((current === '->' || current === '=>') && !state.lambda && state.scopes[0].type == 'coffee' && stream.peek() === '') || style === 'indent') { indent(stream, state); } var delimiter_index = '[({'.indexOf(current); if (delimiter_index !== -1) { indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1)); } if (indentKeywords.exec(current)){ indent(stream, state); } if (current == 'then'){ dedent(stream, state); } if (style === 'dedent') { if (dedent(stream, state)) { return ERRORCLASS; } } delimiter_index = '])}'.indexOf(current); if (delimiter_index !== -1) { if (dedent(stream, state)) { return ERRORCLASS; } } if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'coffee') { if (state.scopes.length > 1) state.scopes.shift(); state.dedent -= 1; } return style; } var external = { startState: function(basecolumn) { return { tokenize: tokenBase, scopes: [{offset:basecolumn || 0, type:'coffee'}], lastToken: null, lambda: false, dedent: 0 }; }, token: function(stream, state) { var style = tokenLexer(stream, state); state.lastToken = {style:style, content: stream.current()}; if (stream.eol() && stream.lambda) { state.lambda = false; } return style; }, indent: function(state, textAfter) { if (state.tokenize != tokenBase) { return 0; } return state.scopes[0].offset; } }; return external; }); CodeMirror.defineMIME('text/x-coffeescript', 'coffeescript'); define("thirdparty/CodeMirror2/mode/coffeescript/coffeescript", function(){}); /** * Author: Hans Engel * Branched from CodeMirror's Scheme mode (by Koh Zi Han, based on implementation by Koh Zi Chun) */ CodeMirror.defineMode("clojure", function (config, mode) { var BUILTIN = "builtin", COMMENT = "comment", STRING = "string", TAG = "tag", ATOM = "atom", NUMBER = "number", BRACKET = "bracket", KEYWORD = "keyword"; var INDENT_WORD_SKIP = 2, KEYWORDS_SKIP = 1; function makeKeywords(str) { var obj = {}, words = str.split(" "); for (var i = 0; i < words.length; ++i) obj[words[i]] = true; return obj; } var atoms = makeKeywords("true false nil"); var keywords = makeKeywords( "defn defn- def def- defonce defmulti defmethod defmacro defstruct deftype defprotocol defrecord defproject deftest slice defalias defhinted defmacro- defn-memo defnk defnk defonce- defunbound defunbound- defvar defvar- let letfn do case cond condp for loop recur when when-not when-let when-first if if-let if-not . .. -> ->> doto and or dosync doseq dotimes dorun doall load import unimport ns in-ns refer try catch finally throw with-open with-local-vars binding gen-class gen-and-load-class gen-and-save-class handler-case handle"); var builtins = makeKeywords( "* *1 *2 *3 *agent* *allow-unresolved-vars* *assert *clojure-version* *command-line-args* *compile-files* *compile-path* *e *err* *file* *flush-on-newline* *in* *macro-meta* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *use-context-classloader* *warn-on-reflection* + - / < <= = == > >= accessor aclone agent agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* butlast byte byte-array bytes case cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec decimal? declare definline defmacro defmethod defmulti defn defn- defonce defstruct delay delay? deliver deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall doc dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq eval even? every? extend extend-protocol extend-type extends? extenders false? ffirst file-seq filter find find-doc find-ns find-var first float float-array float? floats flush fn fn? fnext for force format future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator hash hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map? mapcat max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod name namespace neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext num number? odd? or parents partial partition pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-doc print-dup print-method print-namespace-doc print-simple print-special-doc print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string reify reduce ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure release-pending-sends rem remove remove-method remove-ns repeat repeatedly replace replicate require reset! reset-meta! resolve rest resultset-seq reverse reversible? rseq rsubseq satisfies? second select-keys send send-off seq seq? seque sequence sequential? set set-validator! set? short short-array shorts shutdown-agents slurp some sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-form-anchor special-symbol? split-at split-with str stream? string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync syntax-symbol-anchor take take-last take-nth take-while test the-ns time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-dec unchecked-divide unchecked-inc unchecked-multiply unchecked-negate unchecked-remainder unchecked-subtract underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision xml-seq"); var indentKeys = makeKeywords( // Built-ins "ns fn def defn defmethod bound-fn if if-not case condp when while when-not when-first do future comment doto locking proxy with-open with-precision reify deftype defrecord defprotocol extend extend-protocol extend-type try catch " + // Binding forms "let letfn binding loop for doseq dotimes when-let if-let " + // Data structures "defstruct struct-map assoc " + // clojure.test "testing deftest " + // contrib "handler-case handle dotrace deftrace"); var tests = { digit: /\d/, digit_or_colon: /[\d:]/, hex: /[0-9a-fA-F]/, sign: /[+-]/, exponent: /[eE]/, keyword_char: /[^\s\(\[\;\)\]]/, basic: /[\w\$_\-]/, lang_keyword: /[\w*+!\-_?:\/]/ }; function stateStack(indent, type, prev) { // represents a state stack object this.indent = indent; this.type = type; this.prev = prev; } function pushStack(state, indent, type) { state.indentStack = new stateStack(indent, type, state.indentStack); } function popStack(state) { state.indentStack = state.indentStack.prev; } function isNumber(ch, stream){ // hex if ( ch === '0' && 'x' == stream.peek().toLowerCase() ) { stream.eat('x'); stream.eatWhile(tests.hex); return true; } // leading sign if ( ch == '+' || ch == '-' ) { stream.eat(tests.sign); ch = stream.next(); } if ( tests.digit.test(ch) ) { stream.eat(ch); stream.eatWhile(tests.digit); if ( '.' == stream.peek() ) { stream.eat('.'); stream.eatWhile(tests.digit); } if ( 'e' == stream.peek().toLowerCase() ) { stream.eat(tests.exponent); stream.eat(tests.sign); stream.eatWhile(tests.digit); } return true; } return false; } return { startState: function () { return { indentStack: null, indentation: 0, mode: false }; }, token: function (stream, state) { if (state.indentStack == null && stream.sol()) { // update indentation, but only if indentStack is empty state.indentation = stream.indentation(); } // skip spaces if (stream.eatSpace()) { return null; } var returnType = null; switch(state.mode){ case "string": // multi-line string parsing mode var next, escaped = false; while ((next = stream.next()) != null) { if (next == "\"" && !escaped) { state.mode = false; break; } escaped = !escaped && next == "\\"; } returnType = STRING; // continue on in string mode break; default: // default parsing mode var ch = stream.next(); if (ch == "\"") { state.mode = "string"; returnType = STRING; } else if (ch == "'" && !( tests.digit_or_colon.test(stream.peek()) )) { returnType = ATOM; } else if (ch == ";") { // comment stream.skipToEnd(); // rest of the line is a comment returnType = COMMENT; } else if (isNumber(ch,stream)){ returnType = NUMBER; } else if (ch == "(" || ch == "[") { var keyWord = ''; var indentTemp = stream.column(); /** Either (indent-word .. (non-indent-word .. (;something else, bracket, etc. */ if (ch == "(") while ((letter = stream.eat(tests.keyword_char)) != null) { keyWord += letter; } if (keyWord.length > 0 && indentKeys.propertyIsEnumerable(keyWord)) { // indent-word pushStack(state, indentTemp + INDENT_WORD_SKIP, ch); } else { // non-indent word // we continue eating the spaces stream.eatSpace(); if (stream.eol() || stream.peek() == ";") { // nothing significant after // we restart indentation 1 space after pushStack(state, indentTemp + 1, ch); } else { pushStack(state, indentTemp + stream.current().length, ch); // else we match } } stream.backUp(stream.current().length - 1); // undo all the eating returnType = BRACKET; } else if (ch == ")" || ch == "]") { returnType = BRACKET; if (state.indentStack != null && state.indentStack.type == (ch == ")" ? "(" : "[")) { popStack(state); } } else if ( ch == ":" ) { stream.eatWhile(tests.lang_keyword); return ATOM; } else { stream.eatWhile(tests.basic); if (keywords && keywords.propertyIsEnumerable(stream.current())) { returnType = KEYWORD; } else if (builtins && builtins.propertyIsEnumerable(stream.current())) { returnType = BUILTIN; } else if (atoms && atoms.propertyIsEnumerable(stream.current())) { returnType = ATOM; } else returnType = null; } } return returnType; }, indent: function (state, textAfter) { if (state.indentStack == null) return state.indentation; return state.indentStack.indent; } }; }); CodeMirror.defineMIME("text/x-clojure", "clojure"); define("thirdparty/CodeMirror2/mode/clojure/clojure", function(){}); // CodeMirror2 mode/perl/perl.js (text/x-perl) beta 0.10 (2011-11-08) // This is a part of CodeMirror from https://github.com/sabaca/CodeMirror_mode_perl (mail@sabaca.com) CodeMirror.defineMode("perl",function(config,parserConfig){ // http://perldoc.perl.org var PERL={ // null - magic touch // 1 - keyword // 2 - def // 3 - atom // 4 - operator // 5 - variable-2 (predefined) // [x,y] - x=1,2,3; y=must be defined if x{...} // PERL operators '->' : 4, '++' : 4, '--' : 4, '**' : 4, // ! ~ \ and unary + and - '=~' : 4, '!~' : 4, '*' : 4, '/' : 4, '%' : 4, 'x' : 4, '+' : 4, '-' : 4, '.' : 4, '<<' : 4, '>>' : 4, // named unary operators '<' : 4, '>' : 4, '<=' : 4, '>=' : 4, 'lt' : 4, 'gt' : 4, 'le' : 4, 'ge' : 4, '==' : 4, '!=' : 4, '<=>' : 4, 'eq' : 4, 'ne' : 4, 'cmp' : 4, '~~' : 4, '&' : 4, '|' : 4, '^' : 4, '&&' : 4, '||' : 4, '//' : 4, '..' : 4, '...' : 4, '?' : 4, ':' : 4, '=' : 4, '+=' : 4, '-=' : 4, '*=' : 4, // etc. ??? ',' : 4, '=>' : 4, '::' : 4, // list operators (rightward) 'not' : 4, 'and' : 4, 'or' : 4, 'xor' : 4, // PERL predefined variables (I know, what this is a paranoid idea, but may be needed for people, who learn PERL, and for me as well, ...and may be for you?;) 'BEGIN' : [5,1], 'END' : [5,1], 'PRINT' : [5,1], 'PRINTF' : [5,1], 'GETC' : [5,1], 'READ' : [5,1], 'READLINE' : [5,1], 'DESTROY' : [5,1], 'TIE' : [5,1], 'TIEHANDLE' : [5,1], 'UNTIE' : [5,1], 'STDIN' : 5, 'STDIN_TOP' : 5, 'STDOUT' : 5, 'STDOUT_TOP' : 5, 'STDERR' : 5, 'STDERR_TOP' : 5, '$ARG' : 5, '$_' : 5, '@ARG' : 5, '@_' : 5, '$LIST_SEPARATOR' : 5, '$"' : 5, '$PROCESS_ID' : 5, '$PID' : 5, '$$' : 5, '$REAL_GROUP_ID' : 5, '$GID' : 5, '$(' : 5, '$EFFECTIVE_GROUP_ID' : 5, '$EGID' : 5, '$)' : 5, '$PROGRAM_NAME' : 5, '$0' : 5, '$SUBSCRIPT_SEPARATOR' : 5, '$SUBSEP' : 5, '$;' : 5, '$REAL_USER_ID' : 5, '$UID' : 5, '$<' : 5, '$EFFECTIVE_USER_ID' : 5, '$EUID' : 5, '$>' : 5, '$a' : 5, '$b' : 5, '$COMPILING' : 5, '$^C' : 5, '$DEBUGGING' : 5, '$^D' : 5, '${^ENCODING}' : 5, '$ENV' : 5, '%ENV' : 5, '$SYSTEM_FD_MAX' : 5, '$^F' : 5, '@F' : 5, '${^GLOBAL_PHASE}' : 5, '$^H' : 5, '%^H' : 5, '@INC' : 5, '%INC' : 5, '$INPLACE_EDIT' : 5, '$^I' : 5, '$^M' : 5, '$OSNAME' : 5, '$^O' : 5, '${^OPEN}' : 5, '$PERLDB' : 5, '$^P' : 5, '$SIG' : 5, '%SIG' : 5, '$BASETIME' : 5, '$^T' : 5, '${^TAINT}' : 5, '${^UNICODE}' : 5, '${^UTF8CACHE}' : 5, '${^UTF8LOCALE}' : 5, '$PERL_VERSION' : 5, '$^V' : 5, '${^WIN32_SLOPPY_STAT}' : 5, '$EXECUTABLE_NAME' : 5, '$^X' : 5, '$1' : 5, // - regexp $1, $2... '$MATCH' : 5, '$&' : 5, '${^MATCH}' : 5, '$PREMATCH' : 5, '$`' : 5, '${^PREMATCH}' : 5, '$POSTMATCH' : 5, "$'" : 5, '${^POSTMATCH}' : 5, '$LAST_PAREN_MATCH' : 5, '$+' : 5, '$LAST_SUBMATCH_RESULT' : 5, '$^N' : 5, '@LAST_MATCH_END' : 5, '@+' : 5, '%LAST_PAREN_MATCH' : 5, '%+' : 5, '@LAST_MATCH_START' : 5, '@-' : 5, '%LAST_MATCH_START' : 5, '%-' : 5, '$LAST_REGEXP_CODE_RESULT' : 5, '$^R' : 5, '${^RE_DEBUG_FLAGS}' : 5, '${^RE_TRIE_MAXBUF}' : 5, '$ARGV' : 5, '@ARGV' : 5, 'ARGV' : 5, 'ARGVOUT' : 5, '$OUTPUT_FIELD_SEPARATOR' : 5, '$OFS' : 5, '$,' : 5, '$INPUT_LINE_NUMBER' : 5, '$NR' : 5, '$.' : 5, '$INPUT_RECORD_SEPARATOR' : 5, '$RS' : 5, '$/' : 5, '$OUTPUT_RECORD_SEPARATOR' : 5, '$ORS' : 5, '$\\' : 5, '$OUTPUT_AUTOFLUSH' : 5, '$|' : 5, '$ACCUMULATOR' : 5, '$^A' : 5, '$FORMAT_FORMFEED' : 5, '$^L' : 5, '$FORMAT_PAGE_NUMBER' : 5, '$%' : 5, '$FORMAT_LINES_LEFT' : 5, '$-' : 5, '$FORMAT_LINE_BREAK_CHARACTERS' : 5, '$:' : 5, '$FORMAT_LINES_PER_PAGE' : 5, '$=' : 5, '$FORMAT_TOP_NAME' : 5, '$^' : 5, '$FORMAT_NAME' : 5, '$~' : 5, '${^CHILD_ERROR_NATIVE}' : 5, '$EXTENDED_OS_ERROR' : 5, '$^E' : 5, '$EXCEPTIONS_BEING_CAUGHT' : 5, '$^S' : 5, '$WARNING' : 5, '$^W' : 5, '${^WARNING_BITS}' : 5, '$OS_ERROR' : 5, '$ERRNO' : 5, '$!' : 5, '%OS_ERROR' : 5, '%ERRNO' : 5, '%!' : 5, '$CHILD_ERROR' : 5, '$?' : 5, '$EVAL_ERROR' : 5, '$@' : 5, '$OFMT' : 5, '$#' : 5, '$*' : 5, '$ARRAY_BASE' : 5, '$[' : 5, '$OLD_PERL_VERSION' : 5, '$]' : 5, // PERL blocks 'if' :[1,1], elsif :[1,1], 'else' :[1,1], 'while' :[1,1], unless :[1,1], 'for' :[1,1], foreach :[1,1], // PERL functions 'abs' :1, // - absolute value function accept :1, // - accept an incoming socket connect alarm :1, // - schedule a SIGALRM 'atan2' :1, // - arctangent of Y/X in the range -PI to PI bind :1, // - binds an address to a socket binmode :1, // - prepare binary files for I/O bless :1, // - create an object bootstrap :1, // 'break' :1, // - break out of a "given" block caller :1, // - get context of the current subroutine call chdir :1, // - change your current working directory chmod :1, // - changes the permissions on a list of files chomp :1, // - remove a trailing record separator from a string chop :1, // - remove the last character from a string chown :1, // - change the owership on a list of files chr :1, // - get character this number represents chroot :1, // - make directory new root for path lookups close :1, // - close file (or pipe or socket) handle closedir :1, // - close directory handle connect :1, // - connect to a remote socket 'continue' :[1,1], // - optional trailing block in a while or foreach 'cos' :1, // - cosine function crypt :1, // - one-way passwd-style encryption dbmclose :1, // - breaks binding on a tied dbm file dbmopen :1, // - create binding on a tied dbm file 'default' :1, // defined :1, // - test whether a value, variable, or function is defined 'delete' :1, // - deletes a value from a hash die :1, // - raise an exception or bail out 'do' :1, // - turn a BLOCK into a TERM dump :1, // - create an immediate core dump each :1, // - retrieve the next key/value pair from a hash endgrent :1, // - be done using group file endhostent :1, // - be done using hosts file endnetent :1, // - be done using networks file endprotoent :1, // - be done using protocols file endpwent :1, // - be done using passwd file endservent :1, // - be done using services file eof :1, // - test a filehandle for its end 'eval' :1, // - catch exceptions or compile and run code 'exec' :1, // - abandon this program to run another exists :1, // - test whether a hash key is present exit :1, // - terminate this program 'exp' :1, // - raise I to a power fcntl :1, // - file control system call fileno :1, // - return file descriptor from filehandle flock :1, // - lock an entire file with an advisory lock fork :1, // - create a new process just like this one format :1, // - declare a picture format with use by the write() function formline :1, // - internal function used for formats getc :1, // - get the next character from the filehandle getgrent :1, // - get next group record getgrgid :1, // - get group record given group user ID getgrnam :1, // - get group record given group name gethostbyaddr :1, // - get host record given its address gethostbyname :1, // - get host record given name gethostent :1, // - get next hosts record getlogin :1, // - return who logged in at this tty getnetbyaddr :1, // - get network record given its address getnetbyname :1, // - get networks record given name getnetent :1, // - get next networks record getpeername :1, // - find the other end of a socket connection getpgrp :1, // - get process group getppid :1, // - get parent process ID getpriority :1, // - get current nice value getprotobyname :1, // - get protocol record given name getprotobynumber :1, // - get protocol record numeric protocol getprotoent :1, // - get next protocols record getpwent :1, // - get next passwd record getpwnam :1, // - get passwd record given user login name getpwuid :1, // - get passwd record given user ID getservbyname :1, // - get services record given its name getservbyport :1, // - get services record given numeric port getservent :1, // - get next services record getsockname :1, // - retrieve the sockaddr for a given socket getsockopt :1, // - get socket options on a given socket given :1, // glob :1, // - expand filenames using wildcards gmtime :1, // - convert UNIX time into record or string using Greenwich time 'goto' :1, // - create spaghetti code grep :1, // - locate elements in a list test true against a given criterion hex :1, // - convert a string to a hexadecimal number 'import' :1, // - patch a module's namespace into your own index :1, // - find a substring within a string 'int' :1, // - get the integer portion of a number ioctl :1, // - system-dependent device control system call 'join' :1, // - join a list into a string using a separator keys :1, // - retrieve list of indices from a hash kill :1, // - send a signal to a process or process group last :1, // - exit a block prematurely lc :1, // - return lower-case version of a string lcfirst :1, // - return a string with just the next letter in lower case length :1, // - return the number of bytes in a string 'link' :1, // - create a hard link in the filesytem listen :1, // - register your socket as a server local : 2, // - create a temporary value for a global variable (dynamic scoping) localtime :1, // - convert UNIX time into record or string using local time lock :1, // - get a thread lock on a variable, subroutine, or method 'log' :1, // - retrieve the natural logarithm for a number lstat :1, // - stat a symbolic link m :null, // - match a string with a regular expression pattern map :1, // - apply a change to a list to get back a new list with the changes mkdir :1, // - create a directory msgctl :1, // - SysV IPC message control operations msgget :1, // - get SysV IPC message queue msgrcv :1, // - receive a SysV IPC message from a message queue msgsnd :1, // - send a SysV IPC message to a message queue my : 2, // - declare and assign a local variable (lexical scoping) 'new' :1, // next :1, // - iterate a block prematurely no :1, // - unimport some module symbols or semantics at compile time oct :1, // - convert a string to an octal number open :1, // - open a file, pipe, or descriptor opendir :1, // - open a directory ord :1, // - find a character's numeric representation our : 2, // - declare and assign a package variable (lexical scoping) pack :1, // - convert a list into a binary representation 'package' :1, // - declare a separate global namespace pipe :1, // - open a pair of connected filehandles pop :1, // - remove the last element from an array and return it pos :1, // - find or set the offset for the last/next m//g search print :1, // - output a list to a filehandle printf :1, // - output a formatted list to a filehandle prototype :1, // - get the prototype (if any) of a subroutine push :1, // - append one or more elements to an array q :null, // - singly quote a string qq :null, // - doubly quote a string qr :null, // - Compile pattern quotemeta :null, // - quote regular expression magic characters qw :null, // - quote a list of words qx :null, // - backquote quote a string rand :1, // - retrieve the next pseudorandom number read :1, // - fixed-length buffered input from a filehandle readdir :1, // - get a directory from a directory handle readline :1, // - fetch a record from a file readlink :1, // - determine where a symbolic link is pointing readpipe :1, // - execute a system command and collect standard output recv :1, // - receive a message over a Socket redo :1, // - start this loop iteration over again ref :1, // - find out the type of thing being referenced rename :1, // - change a filename require :1, // - load in external functions from a library at runtime reset :1, // - clear all variables of a given name 'return' :1, // - get out of a function early reverse :1, // - flip a string or a list rewinddir :1, // - reset directory handle rindex :1, // - right-to-left substring search rmdir :1, // - remove a directory s :null, // - replace a pattern with a string say :1, // - print with newline scalar :1, // - force a scalar context seek :1, // - reposition file pointer for random-access I/O seekdir :1, // - reposition directory pointer select :1, // - reset default output or do I/O multiplexing semctl :1, // - SysV semaphore control operations semget :1, // - get set of SysV semaphores semop :1, // - SysV semaphore operations send :1, // - send a message over a socket setgrent :1, // - prepare group file for use sethostent :1, // - prepare hosts file for use setnetent :1, // - prepare networks file for use setpgrp :1, // - set the process group of a process setpriority :1, // - set a process's nice value setprotoent :1, // - prepare protocols file for use setpwent :1, // - prepare passwd file for use setservent :1, // - prepare services file for use setsockopt :1, // - set some socket options shift :1, // - remove the first element of an array, and return it shmctl :1, // - SysV shared memory operations shmget :1, // - get SysV shared memory segment identifier shmread :1, // - read SysV shared memory shmwrite :1, // - write SysV shared memory shutdown :1, // - close down just half of a socket connection 'sin' :1, // - return the sine of a number sleep :1, // - block for some number of seconds socket :1, // - create a socket socketpair :1, // - create a pair of sockets 'sort' :1, // - sort a list of values splice :1, // - add or remove elements anywhere in an array 'split' :1, // - split up a string using a regexp delimiter sprintf :1, // - formatted print into a string 'sqrt' :1, // - square root function srand :1, // - seed the random number generator stat :1, // - get a file's status information state :1, // - declare and assign a state variable (persistent lexical scoping) study :1, // - optimize input data for repeated searches 'sub' :1, // - declare a subroutine, possibly anonymously 'substr' :1, // - get or alter a portion of a stirng symlink :1, // - create a symbolic link to a file syscall :1, // - execute an arbitrary system call sysopen :1, // - open a file, pipe, or descriptor sysread :1, // - fixed-length unbuffered input from a filehandle sysseek :1, // - position I/O pointer on handle used with sysread and syswrite system :1, // - run a separate program syswrite :1, // - fixed-length unbuffered output to a filehandle tell :1, // - get current seekpointer on a filehandle telldir :1, // - get current seekpointer on a directory handle tie :1, // - bind a variable to an object class tied :1, // - get a reference to the object underlying a tied variable time :1, // - return number of seconds since 1970 times :1, // - return elapsed time for self and child processes tr :null, // - transliterate a string truncate :1, // - shorten a file uc :1, // - return upper-case version of a string ucfirst :1, // - return a string with just the next letter in upper case umask :1, // - set file creation mode mask undef :1, // - remove a variable or function definition unlink :1, // - remove one link to a file unpack :1, // - convert binary structure into normal perl variables unshift :1, // - prepend more elements to the beginning of a list untie :1, // - break a tie binding to a variable use :1, // - load in a module at compile time utime :1, // - set a file's last access and modify times values :1, // - return a list of the values in a hash vec :1, // - test or set particular bits in a string wait :1, // - wait for any child process to die waitpid :1, // - wait for a particular child process to die wantarray :1, // - get void vs scalar vs list context of current subroutine call warn :1, // - print debugging info when :1, // write :1, // - print a picture record y :null}; // - transliterate a string var RXstyle="string-2"; var RXmodifiers=/[goseximacplud]/; // NOTE: "m", "s", "y" and "tr" need to correct real modifiers for each regexp type function tokenChain(stream,state,chain,style,tail){ // NOTE: chain.length > 2 is not working now (it's for s[...][...]geos;) state.chain=null; // 12 3tail state.style=null; state.tail=null; state.tokenize=function(stream,state){ var e=false,c,i=0; while(c=stream.next()){ if(c===chain[i]&&!e){ if(chain[++i]!==undefined){ state.chain=chain[i]; state.style=style; state.tail=tail} else if(tail) stream.eatWhile(tail); state.tokenize=tokenPerl; return style} e=!e&&c=="\\"} return style}; return state.tokenize(stream,state)} function tokenSOMETHING(stream,state,string){ state.tokenize=function(stream,state){ if(stream.string==string) state.tokenize=tokenPerl; stream.skipToEnd(); return "string"}; return state.tokenize(stream,state)} function tokenPerl(stream,state){ if(stream.eatSpace()) return null; if(state.chain) return tokenChain(stream,state,state.chain,state.style,state.tail); if(stream.match(/^\-?[\d\.]/,false)) if(stream.match(/^(\-?(\d*\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F]+|0b[01]+|\d+(e[+-]?\d+)?)/)) return 'number'; if(stream.match(/^<<(?=\w)/)){ // NOTE: <<SOMETHING\n...\nSOMETHING\n stream.eatWhile(/\w/); return tokenSOMETHING(stream,state,stream.current().substr(2))} if(stream.sol()&&stream.match(/^\=item(?!\w)/)){// NOTE: \n=item...\n=cut\n return tokenSOMETHING(stream,state,'=cut')} var ch=stream.next(); if(ch=='"'||ch=="'"){ // NOTE: ' or " or <<'SOMETHING'\n...\nSOMETHING\n or <<"SOMETHING"\n...\nSOMETHING\n if(stream.prefix(3)=="<<"+ch){ var p=stream.pos; stream.eatWhile(/\w/); var n=stream.current().substr(1); if(n&&stream.eat(ch)) return tokenSOMETHING(stream,state,n); stream.pos=p} return tokenChain(stream,state,[ch],"string")} if(ch=="q"){ var c=stream.look(-2); if(!(c&&/\w/.test(c))){ c=stream.look(0); if(c=="x"){ c=stream.look(1); if(c=="("){ stream.eatSuffix(2); return tokenChain(stream,state,[")"],RXstyle,RXmodifiers)} if(c=="["){ stream.eatSuffix(2); return tokenChain(stream,state,["]"],RXstyle,RXmodifiers)} if(c=="{"){ stream.eatSuffix(2); return tokenChain(stream,state,["}"],RXstyle,RXmodifiers)} if(c=="<"){ stream.eatSuffix(2); return tokenChain(stream,state,[">"],RXstyle,RXmodifiers)} if(/[\^'"!~\/]/.test(c)){ stream.eatSuffix(1); return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers)}} else if(c=="q"){ c=stream.look(1); if(c=="("){ stream.eatSuffix(2); return tokenChain(stream,state,[")"],"string")} if(c=="["){ stream.eatSuffix(2); return tokenChain(stream,state,["]"],"string")} if(c=="{"){ stream.eatSuffix(2); return tokenChain(stream,state,["}"],"string")} if(c=="<"){ stream.eatSuffix(2); return tokenChain(stream,state,[">"],"string")} if(/[\^'"!~\/]/.test(c)){ stream.eatSuffix(1); return tokenChain(stream,state,[stream.eat(c)],"string")}} else if(c=="w"){ c=stream.look(1); if(c=="("){ stream.eatSuffix(2); return tokenChain(stream,state,[")"],"bracket")} if(c=="["){ stream.eatSuffix(2); return tokenChain(stream,state,["]"],"bracket")} if(c=="{"){ stream.eatSuffix(2); return tokenChain(stream,state,["}"],"bracket")} if(c=="<"){ stream.eatSuffix(2); return tokenChain(stream,state,[">"],"bracket")} if(/[\^'"!~\/]/.test(c)){ stream.eatSuffix(1); return tokenChain(stream,state,[stream.eat(c)],"bracket")}} else if(c=="r"){ c=stream.look(1); if(c=="("){ stream.eatSuffix(2); return tokenChain(stream,state,[")"],RXstyle,RXmodifiers)} if(c=="["){ stream.eatSuffix(2); return tokenChain(stream,state,["]"],RXstyle,RXmodifiers)} if(c=="{"){ stream.eatSuffix(2); return tokenChain(stream,state,["}"],RXstyle,RXmodifiers)} if(c=="<"){ stream.eatSuffix(2); return tokenChain(stream,state,[">"],RXstyle,RXmodifiers)} if(/[\^'"!~\/]/.test(c)){ stream.eatSuffix(1); return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers)}} else if(/[\^'"!~\/(\[{<]/.test(c)){ if(c=="("){ stream.eatSuffix(1); return tokenChain(stream,state,[")"],"string")} if(c=="["){ stream.eatSuffix(1); return tokenChain(stream,state,["]"],"string")} if(c=="{"){ stream.eatSuffix(1); return tokenChain(stream,state,["}"],"string")} if(c=="<"){ stream.eatSuffix(1); return tokenChain(stream,state,[">"],"string")} if(/[\^'"!~\/]/.test(c)){ return tokenChain(stream,state,[stream.eat(c)],"string")}}}} if(ch=="m"){ var c=stream.look(-2); if(!(c&&/\w/.test(c))){ c=stream.eat(/[(\[{<\^'"!~\/]/); if(c){ if(/[\^'"!~\/]/.test(c)){ return tokenChain(stream,state,[c],RXstyle,RXmodifiers)} if(c=="("){ return tokenChain(stream,state,[")"],RXstyle,RXmodifiers)} if(c=="["){ return tokenChain(stream,state,["]"],RXstyle,RXmodifiers)} if(c=="{"){ return tokenChain(stream,state,["}"],RXstyle,RXmodifiers)} if(c=="<"){ return tokenChain(stream,state,[">"],RXstyle,RXmodifiers)}}}} if(ch=="s"){ var c=/[\/>\]})\w]/.test(stream.look(-2)); if(!c){ c=stream.eat(/[(\[{<\^'"!~\/]/); if(c){ if(c=="[") return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); if(c=="{") return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); if(c=="<") return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); if(c=="(") return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers)}}} if(ch=="y"){ var c=/[\/>\]})\w]/.test(stream.look(-2)); if(!c){ c=stream.eat(/[(\[{<\^'"!~\/]/); if(c){ if(c=="[") return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); if(c=="{") return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); if(c=="<") return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); if(c=="(") return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers)}}} if(ch=="t"){ var c=/[\/>\]})\w]/.test(stream.look(-2)); if(!c){ c=stream.eat("r");if(c){ c=stream.eat(/[(\[{<\^'"!~\/]/); if(c){ if(c=="[") return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); if(c=="{") return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); if(c=="<") return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); if(c=="(") return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers)}}}} if(ch=="`"){ return tokenChain(stream,state,[ch],"variable-2")} if(ch=="/"){ if(!/~\s*$/.test(stream.prefix())) return "operator"; else return tokenChain(stream,state,[ch],RXstyle,RXmodifiers)} if(ch=="$"){ var p=stream.pos; if(stream.eatWhile(/\d/)||stream.eat("{")&&stream.eatWhile(/\d/)&&stream.eat("}")) return "variable-2"; else stream.pos=p} if(/[$@%]/.test(ch)){ var p=stream.pos; if(stream.eat("^")&&stream.eat(/[A-Z]/)||!/[@$%&]/.test(stream.look(-2))&&stream.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)){ var c=stream.current(); if(PERL[c]) return "variable-2"} stream.pos=p} if(/[$@%&]/.test(ch)){ if(stream.eatWhile(/[\w$\[\]]/)||stream.eat("{")&&stream.eatWhile(/[\w$\[\]]/)&&stream.eat("}")){ var c=stream.current(); if(PERL[c]) return "variable-2"; else return "variable"}} if(ch=="#"){ if(stream.look(-2)!="$"){ stream.skipToEnd(); return "comment"}} if(/[:+\-\^*$&%@=<>!?|\/~\.]/.test(ch)){ var p=stream.pos; stream.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/); if(PERL[stream.current()]) return "operator"; else stream.pos=p} if(ch=="_"){ if(stream.pos==1){ if(stream.suffix(6)=="_END__"){ return tokenChain(stream,state,['\0'],"comment")} else if(stream.suffix(7)=="_DATA__"){ return tokenChain(stream,state,['\0'],"variable-2")} else if(stream.suffix(7)=="_C__"){ return tokenChain(stream,state,['\0'],"string")}}} if(/\w/.test(ch)){ var p=stream.pos; if(stream.look(-2)=="{"&&(stream.look(0)=="}"||stream.eatWhile(/\w/)&&stream.look(0)=="}")) return "string"; else stream.pos=p} if(/[A-Z]/.test(ch)){ var l=stream.look(-2); var p=stream.pos; stream.eatWhile(/[A-Z_]/); if(/[\da-z]/.test(stream.look(0))){ stream.pos=p} else{ var c=PERL[stream.current()]; if(!c) return "meta"; if(c[1]) c=c[0]; if(l!=":"){ if(c==1) return "keyword"; else if(c==2) return "def"; else if(c==3) return "atom"; else if(c==4) return "operator"; else if(c==5) return "variable-2"; else return "meta"} else return "meta"}} if(/[a-zA-Z_]/.test(ch)){ var l=stream.look(-2); stream.eatWhile(/\w/); var c=PERL[stream.current()]; if(!c) return "meta"; if(c[1]) c=c[0]; if(l!=":"){ if(c==1) return "keyword"; else if(c==2) return "def"; else if(c==3) return "atom"; else if(c==4) return "operator"; else if(c==5) return "variable-2"; else return "meta"} else return "meta"} return null} return{ startState:function(){ return{ tokenize:tokenPerl, chain:null, style:null, tail:null}}, token:function(stream,state){ return (state.tokenize||tokenPerl)(stream,state)}, electricChars:"{}"}}); CodeMirror.defineMIME("text/x-perl", "perl"); // it's like "peek", but need for look-ahead or look-behind if index < 0 CodeMirror.StringStream.prototype.look=function(c){ return this.string.charAt(this.pos+(c||0))}; // return a part of prefix of current stream from current position CodeMirror.StringStream.prototype.prefix=function(c){ if(c){ var x=this.pos-c; return this.string.substr((x>=0?x:0),c)} else{ return this.string.substr(0,this.pos-1)}}; // return a part of suffix of current stream from current position CodeMirror.StringStream.prototype.suffix=function(c){ var y=this.string.length; var x=y-this.pos+1; return this.string.substr(this.pos,(c&&c<y?c:x))}; // return a part of suffix of current stream from current position and change current position CodeMirror.StringStream.prototype.nsuffix=function(c){ var p=this.pos; var l=c||(this.string.length-this.pos+1); this.pos+=l; return this.string.substr(p,l)}; // eating and vomiting a part of stream from current position CodeMirror.StringStream.prototype.eatSuffix=function(c){ var x=this.pos+c; var y; if(x<=0) this.pos=0; else if(x>=(y=this.string.length-1)) this.pos=y; else this.pos=x}; define("thirdparty/CodeMirror2/mode/perl/perl", function(){}); CodeMirror.defineMode("ruby", function(config, parserConfig) { function wordObj(words) { var o = {}; for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true; return o; } var keywords = wordObj([ "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else", "elsif", "END", "end", "ensure", "false", "for", "if", "in", "module", "next", "not", "or", "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", "until", "when", "while", "yield", "nil", "raise", "throw", "catch", "fail", "loop", "callcc", "caller", "lambda", "proc", "public", "protected", "private", "require", "load", "require_relative", "extend", "autoload" ]); var indentWords = wordObj(["def", "class", "case", "for", "while", "do", "module", "then", "catch", "loop", "proc", "begin"]); var dedentWords = wordObj(["end", "until"]); var matching = {"[": "]", "{": "}", "(": ")"}; var curPunc; function chain(newtok, stream, state) { state.tokenize.push(newtok); return newtok(stream, state); } function tokenBase(stream, state) { curPunc = null; if (stream.sol() && stream.match("=begin") && stream.eol()) { state.tokenize.push(readBlockComment); return "comment"; } if (stream.eatSpace()) return null; var ch = stream.next(), m; if (ch == "`" || ch == "'" || ch == '"' || (ch == "/" && !stream.eol() && stream.peek() != " ")) { return chain(readQuoted(ch, "string", ch == '"' || ch == "`"), stream, state); } else if (ch == "%") { var style, embed = false; if (stream.eat("s")) style = "atom"; else if (stream.eat(/[WQ]/)) { style = "string"; embed = true; } else if (stream.eat(/[wxqr]/)) style = "string"; var delim = stream.eat(/[^\w\s]/); if (!delim) return "operator"; if (matching.propertyIsEnumerable(delim)) delim = matching[delim]; return chain(readQuoted(delim, style, embed, true), stream, state); } else if (ch == "#") { stream.skipToEnd(); return "comment"; } else if (ch == "<" && (m = stream.match(/^<-?[\`\"\']?([a-zA-Z_?]\w*)[\`\"\']?(?:;|$)/))) { return chain(readHereDoc(m[1]), stream, state); } else if (ch == "0") { if (stream.eat("x")) stream.eatWhile(/[\da-fA-F]/); else if (stream.eat("b")) stream.eatWhile(/[01]/); else stream.eatWhile(/[0-7]/); return "number"; } else if (/\d/.test(ch)) { stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?/); return "number"; } else if (ch == "?") { while (stream.match(/^\\[CM]-/)) {} if (stream.eat("\\")) stream.eatWhile(/\w/); else stream.next(); return "string"; } else if (ch == ":") { if (stream.eat("'")) return chain(readQuoted("'", "atom", false), stream, state); if (stream.eat('"')) return chain(readQuoted('"', "atom", true), stream, state); stream.eatWhile(/[\w\?]/); return "atom"; } else if (ch == "@") { stream.eat("@"); stream.eatWhile(/[\w\?]/); return "variable-2"; } else if (ch == "$") { stream.next(); stream.eatWhile(/[\w\?]/); return "variable-3"; } else if (/\w/.test(ch)) { stream.eatWhile(/[\w\?]/); if (stream.eat(":")) return "atom"; return "ident"; } else if (ch == "|" && (state.varList || state.lastTok == "{" || state.lastTok == "do")) { curPunc = "|"; return null; } else if (/[\(\)\[\]{}\\;]/.test(ch)) { curPunc = ch; return null; } else if (ch == "-" && stream.eat(">")) { return "arrow"; } else if (/[=+\-\/*:\.^%<>~|]/.test(ch)) { stream.eatWhile(/[=+\-\/*:\.^%<>~|]/); return "operator"; } else { return null; } } function tokenBaseUntilBrace() { var depth = 1; return function(stream, state) { if (stream.peek() == "}") { depth--; if (depth == 0) { state.tokenize.pop(); return state.tokenize[state.tokenize.length-1](stream, state); } } else if (stream.peek() == "{") { depth++; } return tokenBase(stream, state); }; } function readQuoted(quote, style, embed, unescaped) { return function(stream, state) { var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && (unescaped || !escaped)) { state.tokenize.pop(); break; } if (embed && ch == "#" && !escaped && stream.eat("{")) { state.tokenize.push(tokenBaseUntilBrace(arguments.callee)); break; } escaped = !escaped && ch == "\\"; } return style; }; } function readHereDoc(phrase) { return function(stream, state) { if (stream.match(phrase)) state.tokenize.pop(); else stream.skipToEnd(); return "string"; }; } function readBlockComment(stream, state) { if (stream.sol() && stream.match("=end") && stream.eol()) state.tokenize.pop(); stream.skipToEnd(); return "comment"; } return { startState: function() { return {tokenize: [tokenBase], indented: 0, context: {type: "top", indented: -config.indentUnit}, continuedLine: false, lastTok: null, varList: false}; }, token: function(stream, state) { if (stream.sol()) state.indented = stream.indentation(); var style = state.tokenize[state.tokenize.length-1](stream, state), kwtype; if (style == "ident") { var word = stream.current(); style = keywords.propertyIsEnumerable(stream.current()) ? "keyword" : /^[A-Z]/.test(word) ? "tag" : (state.lastTok == "def" || state.lastTok == "class" || state.varList) ? "def" : "variable"; if (indentWords.propertyIsEnumerable(word)) kwtype = "indent"; else if (dedentWords.propertyIsEnumerable(word)) kwtype = "dedent"; else if ((word == "if" || word == "unless") && stream.column() == stream.indentation()) kwtype = "indent"; } if (curPunc || (style && style != "comment")) state.lastTok = word || curPunc || style; if (curPunc == "|") state.varList = !state.varList; if (kwtype == "indent" || /[\(\[\{]/.test(curPunc)) state.context = {prev: state.context, type: curPunc || style, indented: state.indented}; else if ((kwtype == "dedent" || /[\)\]\}]/.test(curPunc)) && state.context.prev) state.context = state.context.prev; if (stream.eol()) state.continuedLine = (curPunc == "\\" || style == "operator"); return style; }, indent: function(state, textAfter) { if (state.tokenize[state.tokenize.length-1] != tokenBase) return 0; var firstChar = textAfter && textAfter.charAt(0); var ct = state.context; var closing = ct.type == matching[firstChar] || ct.type == "keyword" && /^(?:end|until|else|elsif|when|rescue)\b/.test(textAfter); return ct.indented + (closing ? 0 : config.indentUnit) + (state.continuedLine ? config.indentUnit : 0); }, electricChars: "}de" // enD and rescuE }; }); CodeMirror.defineMIME("text/x-ruby", "ruby"); define("thirdparty/CodeMirror2/mode/ruby/ruby", function(){}); /* * MySQL Mode for CodeMirror 2 by MySQL-Tools * @author James Thorne (partydroid) * @link http://github.com/partydroid/MySQL-Tools * @link http://mysqltools.org * @version 02/Jan/2012 */ CodeMirror.defineMode("mysql", function(config) { var indentUnit = config.indentUnit; var curPunc; function wordRegexp(words) { return new RegExp("^(?:" + words.join("|") + ")$", "i"); } var ops = wordRegexp(["str", "lang", "langmatches", "datatype", "bound", "sameterm", "isiri", "isuri", "isblank", "isliteral", "union", "a"]); var keywords = wordRegexp([ ('ACCESSIBLE'),('ALTER'),('AS'),('BEFORE'),('BINARY'),('BY'),('CASE'),('CHARACTER'),('COLUMN'),('CONTINUE'),('CROSS'),('CURRENT_TIMESTAMP'),('DATABASE'),('DAY_MICROSECOND'),('DEC'),('DEFAULT'), ('DESC'),('DISTINCT'),('DOUBLE'),('EACH'),('ENCLOSED'),('EXIT'),('FETCH'),('FLOAT8'),('FOREIGN'),('GRANT'),('HIGH_PRIORITY'),('HOUR_SECOND'),('IN'),('INNER'),('INSERT'),('INT2'),('INT8'), ('INTO'),('JOIN'),('KILL'),('LEFT'),('LINEAR'),('LOCALTIME'),('LONG'),('LOOP'),('MATCH'),('MEDIUMTEXT'),('MINUTE_SECOND'),('NATURAL'),('NULL'),('OPTIMIZE'),('OR'),('OUTER'),('PRIMARY'), ('RANGE'),('READ_WRITE'),('REGEXP'),('REPEAT'),('RESTRICT'),('RIGHT'),('SCHEMAS'),('SENSITIVE'),('SHOW'),('SPECIFIC'),('SQLSTATE'),('SQL_CALC_FOUND_ROWS'),('STARTING'),('TERMINATED'), ('TINYINT'),('TRAILING'),('UNDO'),('UNLOCK'),('USAGE'),('UTC_DATE'),('VALUES'),('VARCHARACTER'),('WHERE'),('WRITE'),('ZEROFILL'),('ALL'),('AND'),('ASENSITIVE'),('BIGINT'),('BOTH'),('CASCADE'), ('CHAR'),('COLLATE'),('CONSTRAINT'),('CREATE'),('CURRENT_TIME'),('CURSOR'),('DAY_HOUR'),('DAY_SECOND'),('DECLARE'),('DELETE'),('DETERMINISTIC'),('DIV'),('DUAL'),('ELSEIF'),('EXISTS'),('FALSE'), ('FLOAT4'),('FORCE'),('FULLTEXT'),('HAVING'),('HOUR_MINUTE'),('IGNORE'),('INFILE'),('INSENSITIVE'),('INT1'),('INT4'),('INTERVAL'),('ITERATE'),('KEYS'),('LEAVE'),('LIMIT'),('LOAD'),('LOCK'), ('LONGTEXT'),('MASTER_SSL_VERIFY_SERVER_CERT'),('MEDIUMINT'),('MINUTE_MICROSECOND'),('MODIFIES'),('NO_WRITE_TO_BINLOG'),('ON'),('OPTIONALLY'),('OUT'),('PRECISION'),('PURGE'),('READS'), ('REFERENCES'),('RENAME'),('REQUIRE'),('REVOKE'),('SCHEMA'),('SELECT'),('SET'),('SPATIAL'),('SQLEXCEPTION'),('SQL_BIG_RESULT'),('SSL'),('TABLE'),('TINYBLOB'),('TO'),('TRUE'),('UNIQUE'), ('UPDATE'),('USING'),('UTC_TIMESTAMP'),('VARCHAR'),('WHEN'),('WITH'),('YEAR_MONTH'),('ADD'),('ANALYZE'),('ASC'),('BETWEEN'),('BLOB'),('CALL'),('CHANGE'),('CHECK'),('CONDITION'),('CONVERT'), ('CURRENT_DATE'),('CURRENT_USER'),('DATABASES'),('DAY_MINUTE'),('DECIMAL'),('DELAYED'),('DESCRIBE'),('DISTINCTROW'),('DROP'),('ELSE'),('ESCAPED'),('EXPLAIN'),('FLOAT'),('FOR'),('FROM'), ('GROUP'),('HOUR_MICROSECOND'),('IF'),('INDEX'),('INOUT'),('INT'),('INT3'),('INTEGER'),('IS'),('KEY'),('LEADING'),('LIKE'),('LINES'),('LOCALTIMESTAMP'),('LONGBLOB'),('LOW_PRIORITY'), ('MEDIUMBLOB'),('MIDDLEINT'),('MOD'),('NOT'),('NUMERIC'),('OPTION'),('ORDER'),('OUTFILE'),('PROCEDURE'),('READ'),('REAL'),('RELEASE'),('REPLACE'),('RETURN'),('RLIKE'),('SECOND_MICROSECOND'), ('SEPARATOR'),('SMALLINT'),('SQL'),('SQLWARNING'),('SQL_SMALL_RESULT'),('STRAIGHT_JOIN'),('THEN'),('TINYTEXT'),('TRIGGER'),('UNION'),('UNSIGNED'),('USE'),('UTC_TIME'),('VARBINARY'),('VARYING'), ('WHILE'),('XOR'),('FULL'),('COLUMNS'),('MIN'),('MAX'),('STDEV'),('COUNT') ]); var operatorChars = /[*+\-<>=&|]/; function tokenBase(stream, state) { var ch = stream.next(); curPunc = null; if (ch == "$" || ch == "?") { stream.match(/^[\w\d]*/); return "variable-2"; } else if (ch == "<" && !stream.match(/^[\s\u00a0=]/, false)) { stream.match(/^[^\s\u00a0>]*>?/); return "atom"; } else if (ch == "\"" || ch == "'") { state.tokenize = tokenLiteral(ch); return state.tokenize(stream, state); } else if (ch == "`") { state.tokenize = tokenOpLiteral(ch); return state.tokenize(stream, state); } else if (/[{}\(\),\.;\[\]]/.test(ch)) { curPunc = ch; return null; } else if (ch == "-") { ch2 = stream.next(); if(ch2=="-") { stream.skipToEnd(); return "comment"; } } else if (operatorChars.test(ch)) { stream.eatWhile(operatorChars); return null; } else if (ch == ":") { stream.eatWhile(/[\w\d\._\-]/); return "atom"; } else { stream.eatWhile(/[_\w\d]/); if (stream.eat(":")) { stream.eatWhile(/[\w\d_\-]/); return "atom"; } var word = stream.current(), type; if (ops.test(word)) return null; else if (keywords.test(word)) return "keyword"; else return "variable"; } } function tokenLiteral(quote) { return function(stream, state) { var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && !escaped) { state.tokenize = tokenBase; break; } escaped = !escaped && ch == "\\"; } return "string"; }; } function tokenOpLiteral(quote) { return function(stream, state) { var escaped = false, ch; while ((ch = stream.next()) != null) { if (ch == quote && !escaped) { state.tokenize = tokenBase; break; } escaped = !escaped && ch == "\\"; } return "variable-2"; }; } function pushContext(state, type, col) { state.context = {prev: state.context, indent: state.indent, col: col, type: type}; } function popContext(state) { state.indent = state.context.indent; state.context = state.context.prev; } return { startState: function(base) { return {tokenize: tokenBase, context: null, indent: 0, col: 0}; }, token: function(stream, state) { if (stream.sol()) { if (state.context && state.context.align == null) state.context.align = false; state.indent = stream.indentation(); } if (stream.eatSpace()) return null; var style = state.tokenize(stream, state); if (style != "comment" && state.context && state.context.align == null && state.context.type != "pattern") { state.context.align = true; } if (curPunc == "(") pushContext(state, ")", stream.column()); else if (curPunc == "[") pushContext(state, "]", stream.column()); else if (curPunc == "{") pushContext(state, "}", stream.column()); else if (/[\]\}\)]/.test(curPunc)) { while (state.context && state.context.type == "pattern") popContext(state); if (state.context && curPunc == state.context.type) popContext(state); } else if (curPunc == "." && state.context && state.context.type == "pattern") popContext(state); else if (/atom|string|variable/.test(style) && state.context) { if (/[\}\]]/.test(state.context.type)) pushContext(state, "pattern", stream.column()); else if (state.context.type == "pattern" && !state.context.align) { state.context.align = true; state.context.col = stream.column(); } } return style; }, indent: function(state, textAfter) { var firstChar = textAfter && textAfter.charAt(0); var context = state.context; if (/[\]\}]/.test(firstChar)) while (context && context.type == "pattern") context = context.prev; var closing = context && firstChar == context.type; if (!context) return 0; else if (context.type == "pattern") return context.col; else if (context.align) return context.col + (closing ? 0 : 1); else return context.indent + (closing ? 0 : indentUnit); } }; }); CodeMirror.defineMIME("text/x-mysql", "mysql"); define("thirdparty/CodeMirror2/mode/mysql/mysql", function(){}); CodeMirror.defineMode("diff", function() { var TOKEN_NAMES = { '+': 'tag', '-': 'string', '@': 'meta' }; return { token: function(stream) { var tw_pos = stream.string.search(/[\t ]+?$/); if (!stream.sol() || tw_pos === 0) { stream.skipToEnd(); return ("error " + ( TOKEN_NAMES[stream.string.charAt(0)] || '')).replace(/ $/, ''); } var token_name = TOKEN_NAMES[stream.peek()] || stream.skipToEnd(); if (tw_pos === -1) { stream.skipToEnd(); } else { stream.pos = tw_pos; } return token_name; } }; }); CodeMirror.defineMIME("text/x-diff", "diff"); define("thirdparty/CodeMirror2/mode/diff/diff", function(){}); CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) { var htmlMode = CodeMirror.getMode(cmCfg, { name: 'xml', htmlMode: true }); var header = 'header' , code = 'comment' , quote = 'quote' , list = 'string' , hr = 'hr' , linktext = 'link' , linkhref = 'string' , em = 'em' , strong = 'strong' , emstrong = 'emstrong'; var hrRE = /^([*\-=_])(?:\s*\1){2,}\s*$/ , ulRE = /^[*\-+]\s+/ , olRE = /^[0-9]+\.\s+/ , headerRE = /^(?:\={3,}|-{3,})$/ , textRE = /^[^\[*_\\<>`]+/; function switchInline(stream, state, f) { state.f = state.inline = f; return f(stream, state); } function switchBlock(stream, state, f) { state.f = state.block = f; return f(stream, state); } // Blocks function blankLine(state) { // Reset EM state state.em = false; // Reset STRONG state state.strong = false; return null; } function blockNormal(stream, state) { var match; if (state.indentationDiff >= 4) { state.indentation -= state.indentationDiff; stream.skipToEnd(); return code; } else if (stream.eatSpace()) { return null; } else if (stream.peek() === '#' || stream.match(headerRE)) { state.header = true; } else if (stream.eat('>')) { state.indentation++; state.quote = true; } else if (stream.peek() === '[') { return switchInline(stream, state, footnoteLink); } else if (stream.match(hrRE, true)) { return hr; } else if (match = stream.match(ulRE, true) || stream.match(olRE, true)) { state.indentation += match[0].length; return list; } return switchInline(stream, state, state.inline); } function htmlBlock(stream, state) { var style = htmlMode.token(stream, state.htmlState); if (style === 'tag' && state.htmlState.type !== 'openTag' && !state.htmlState.context) { state.f = inlineNormal; state.block = blockNormal; } return style; } // Inline function getType(state) { var styles = []; if (state.strong) { styles.push(state.em ? emstrong : strong); } else if (state.em) { styles.push(em); } if (state.header) { styles.push(header); } if (state.quote) { styles.push(quote); } return styles.length ? styles.join(' ') : null; } function handleText(stream, state) { if (stream.match(textRE, true)) { return getType(state); } return undefined; } function inlineNormal(stream, state) { var style = state.text(stream, state) if (typeof style !== 'undefined') return style; var ch = stream.next(); if (ch === '\\') { stream.next(); return getType(state); } if (ch === '`') { return switchInline(stream, state, inlineElement(code, '`')); } if (ch === '[') { return switchInline(stream, state, linkText); } if (ch === '<' && stream.match(/^\w/, false)) { stream.backUp(1); return switchBlock(stream, state, htmlBlock); } var t = getType(state); if (ch === '*' || ch === '_') { if (stream.eat(ch)) { return (state.strong = !state.strong) ? getType(state) : t; } return (state.em = !state.em) ? getType(state) : t; } return getType(state); } function linkText(stream, state) { while (!stream.eol()) { var ch = stream.next(); if (ch === '\\') stream.next(); if (ch === ']') { state.inline = state.f = linkHref; return linktext; } } return linktext; } function linkHref(stream, state) { stream.eatSpace(); var ch = stream.next(); if (ch === '(' || ch === '[') { return switchInline(stream, state, inlineElement(linkhref, ch === '(' ? ')' : ']')); } return 'error'; } function footnoteLink(stream, state) { if (stream.match(/^[^\]]*\]:/, true)) { state.f = footnoteUrl; return linktext; } return switchInline(stream, state, inlineNormal); } function footnoteUrl(stream, state) { stream.eatSpace(); stream.match(/^[^\s]+/, true); state.f = state.inline = inlineNormal; return linkhref; } function inlineRE(endChar) { if (!inlineRE[endChar]) { // match any not-escaped-non-endChar and any escaped char // then match endChar or eol inlineRE[endChar] = new RegExp('^(?:[^\\\\\\' + endChar + ']|\\\\.)*(?:\\' + endChar + '|$)'); } return inlineRE[endChar]; } function inlineElement(type, endChar, next) { next = next || inlineNormal; return function(stream, state) { stream.match(inlineRE(endChar)); state.inline = state.f = next; return type; }; } return { startState: function() { return { f: blockNormal, block: blockNormal, htmlState: htmlMode.startState(), indentation: 0, inline: inlineNormal, text: handleText, em: false, strong: false, header: false, quote: false }; }, copyState: function(s) { return { f: s.f, block: s.block, htmlState: CodeMirror.copyState(htmlMode, s.htmlState), indentation: s.indentation, inline: s.inline, text: s.text, em: s.em, strong: s.strong, header: s.header, quote: s.quote }; }, token: function(stream, state) { if (stream.sol()) { if (stream.match(/^\s*$/, true)) { return blankLine(state); } // Reset state.header state.header = false; // Reset state.quote state.quote = false; state.f = state.block; var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length; state.indentationDiff = indentation - state.indentation; state.indentation = indentation; if (indentation > 0) { return null; } } return state.f(stream, state); }, blankLine: blankLine, getType: getType }; }, "xml"); CodeMirror.defineMIME("text/x-markdown", "markdown"); define("thirdparty/CodeMirror2/mode/markdown/markdown", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, PathUtils, FileError, brackets */ /** * Set of utilites for working with the code editor */ define('editor/EditorUtils',['require','exports','module','thirdparty/path-utils/path-utils.min','thirdparty/CodeMirror2/mode/xml/xml','thirdparty/CodeMirror2/mode/javascript/javascript','thirdparty/CodeMirror2/mode/css/css','thirdparty/CodeMirror2/mode/less/less','thirdparty/CodeMirror2/mode/htmlmixed/htmlmixed','thirdparty/CodeMirror2/mode/clike/clike','thirdparty/CodeMirror2/mode/php/php','thirdparty/CodeMirror2/mode/coffeescript/coffeescript','thirdparty/CodeMirror2/mode/clojure/clojure','thirdparty/CodeMirror2/mode/perl/perl','thirdparty/CodeMirror2/mode/ruby/ruby','thirdparty/CodeMirror2/mode/mysql/mysql','thirdparty/CodeMirror2/mode/diff/diff','thirdparty/CodeMirror2/mode/markdown/markdown'],function (require, exports, module) { require("thirdparty/path-utils/path-utils.min"); require("thirdparty/CodeMirror2/mode/xml/xml"); require("thirdparty/CodeMirror2/mode/javascript/javascript"); require("thirdparty/CodeMirror2/mode/css/css"); require("thirdparty/CodeMirror2/mode/less/less"); require("thirdparty/CodeMirror2/mode/htmlmixed/htmlmixed"); require("thirdparty/CodeMirror2/mode/clike/clike"); require("thirdparty/CodeMirror2/mode/php/php"); require("thirdparty/CodeMirror2/mode/coffeescript/coffeescript"); require("thirdparty/CodeMirror2/mode/clojure/clojure"); require("thirdparty/CodeMirror2/mode/perl/perl"); require("thirdparty/CodeMirror2/mode/ruby/ruby"); require("thirdparty/CodeMirror2/mode/mysql/mysql"); require("thirdparty/CodeMirror2/mode/diff/diff"); require("thirdparty/CodeMirror2/mode/markdown/markdown"); /** * @private * Given a file URL, determines the mode to use based * off the file's extension. * @param {string} fileUrl A cannonical file URL to extract the extension from */ function getModeFromFileExtension(fileUrl) { var ext = PathUtils.filenameExtension(fileUrl); //incase the arg is just the ext if (!ext) { ext = fileUrl; } if (ext.charAt(0) === ".") { ext = ext.substr(1); } switch (ext) { case "js": return "javascript"; case "json": return {name: "javascript", json: true}; case "css": return "css"; case "less": return "less"; case "html": case "htm": case "xhtml": case "cfm": case "cfc": return "htmlmixed"; case "xml": return "xml"; case "php": case "php3": case "php4": case "php5": case "phtm": case "phtml": return "php"; case "cc": case "cp": case "cpp": case "c++": case "cxx": case "hh": case "hpp": case "hxx": case "h++": case "ii": return "text/x-c++src"; case "c": case "h": case "i": return "text/x-csrc"; case "cs": return "text/x-csharp"; case "java": return "text/x-java"; case "coffee": return "coffeescript"; case "clj": return "clojure"; case "pl": return "perl"; case "rb": return "ruby"; case "sql": return "mysql"; case "diff": case "patch": return "diff"; case "md": return "markdown"; default: console.log("Called EditorUtils.js _getModeFromFileExtensions with an unhandled file extension: " + ext); return ""; } } // Define public API exports.getModeFromFileExtension = getModeFromFileExtension; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ /** * EditorManager owns the UI for the editor area. This essentially mirrors the 'current document' * property maintained by DocumentManager's model. * * Note that there is a little bit of unusual overlap between EditorManager and DocumentManager: * because the Document state is actually stored in the CodeMirror editor UI, DocumentManager is * not a pure headless model. Each Document encapsulates an editor instance, and thus EditorManager * must have some knowledge about Document's internal state (we access its _editor property). * * This module dispatches the following events: * - focusedEditorChange -- When the focused editor (full or inline) changes and size/visibility are complete. */ define('editor/EditorManager',['require','exports','module','file/FileUtils','command/Commands','command/CommandManager','document/DocumentManager','utils/PerfUtils','editor/Editor','editor/InlineTextEditor','editor/EditorUtils','utils/ViewUtils','strings'],function (require, exports, module) { // Load dependent modules var FileUtils = require("file/FileUtils"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), DocumentManager = require("document/DocumentManager"), PerfUtils = require("utils/PerfUtils"), Editor = require("editor/Editor").Editor, InlineTextEditor = require("editor/InlineTextEditor").InlineTextEditor, EditorUtils = require("editor/EditorUtils"), ViewUtils = require("utils/ViewUtils"), Strings = require("strings"); /** @type {jQueryObject} DOM node that contains all editors (visible and hidden alike) */ var _editorHolder = null; /** @type {Editor} */ var _currentEditor = null; /** @type {Document} */ var _currentEditorsDocument = null; /** @type {number} Used by {@link #_updateEditorSize()} */ var _resizeTimeout = null; /** * Registered inline-editor widget providers. See {@link #registerInlineEditProvider()}. * @type {Array.<function(...)>} */ var _inlineEditProviders = []; /** * Adds keyboard command handlers to an Editor instance. * @param {Editor} editor * @param {!Object.<string,function(Editor)>} to destination key mapping * @param {!Object.<string,function(Editor)>} from source key mapping */ function mergeExtraKeys(editor, to, from) { // Merge in the additionalKeys we were passed function wrapEventHandler(externalHandler) { return function (instance) { externalHandler(editor); }; } var key; for (key in from) { if (from.hasOwnProperty(key)) { if (to.hasOwnProperty(key)) { console.log("Warning: overwriting standard Editor shortcut " + key); } to[key] = (editor !== null) ? wrapEventHandler(from[key]) : from[key]; } } } /** * Creates a new Editor bound to the given Document. The editor's mode is inferred based on the * file extension. The editor is appended to the given container as a visible child. * @param {!Document} doc Document for the Editor's content * @param {!boolean} makeMasterEditor If true, the Editor will set itself as the private "master" * Editor for the Document. If false, the Editor will attach to the Document as a "slave." * @param {!jQueryObject} container Container to add the editor to. * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document * to display in this editor. Inclusive. * @return {Editor} the newly created editor. */ function _createEditorForDocument(doc, makeMasterEditor, container, range, additionalKeys) { var mode = EditorUtils.getModeFromFileExtension(doc.file.fullPath); return new Editor(doc, makeMasterEditor, mode, container, additionalKeys, range); } /** * @private * Bound to Ctrl+E on outermost editors. * @param {!Editor} editor the candidate host editor * @return {$.Promise} a promise that will be resolved when an InlineWidget * is created or rejected when no inline editors are available. */ function _openInlineWidget(editor) { PerfUtils.markStart(PerfUtils.INLINE_EDITOR_OPEN); // Run through inline-editor providers until one responds var pos = editor.getCursorPos(), inlinePromise, i, result = new $.Deferred(); for (i = 0; i < _inlineEditProviders.length && !inlinePromise; i++) { var provider = _inlineEditProviders[i]; inlinePromise = provider(editor, pos); } // If one of them will provide a widget, show it inline once ready if (inlinePromise) { inlinePromise.done(function (inlineWidget) { editor.addInlineWidget(pos, inlineWidget); PerfUtils.addMeasurement(PerfUtils.INLINE_EDITOR_OPEN); result.resolve(); }).fail(function () { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_EDITOR_OPEN); result.reject(); }); } else { // terminate timer that was started above PerfUtils.finalizeMeasurement(PerfUtils.INLINE_EDITOR_OPEN); result.reject(); } return result.promise(); } /** * Removes the given widget UI from the given hostEditor (agnostic of what the widget's content * is). The widget's onClosed() callback will be run as a result. * @param {!Editor} hostEditor The editor containing the widget. * @param {!InlineWidget} inlineWidget The inline widget to close. * @param {!boolean} moveFocus If true, focuses hostEditor and ensures the cursor position lies * near the inline's location. */ function closeInlineWidget(hostEditor, inlineWidget, moveFocus) { if (moveFocus) { // Place cursor back on the line just above the inline (the line from which it was opened) // If cursor's already on that line, leave it be to preserve column position var widgetLine = hostEditor._codeMirror.getInlineWidgetInfo(inlineWidget.id).line; var cursorLine = hostEditor.getCursorPos().line; if (cursorLine !== widgetLine) { hostEditor.setCursorPos({ line: widgetLine, pos: 0 }); } hostEditor.focus(); } hostEditor.removeInlineWidget(inlineWidget); } /** * Registers a new inline provider. When _openInlineWidget() is called each registered inline * widget is called and asked if it wants to provide an inline widget given the current cursor * location and document. * @param {function} provider * Parameters: * {!Editor} editor, {!{line:Number, ch:Number}} pos * * Returns: * {$.Promise} a promise that will be resolved with an inlineWidget * or null to indicate the provider doesn't create an editor in this case */ function registerInlineEditProvider(provider) { _inlineEditProviders.push(provider); } /** * @private * Given a host editor, return a list of all Editors in all its open inline widgets. (Ignoring * any other inline widgets that might be open but don't contain Editors). * @param {!Editor} hostEditor * @return {Array.<Editor>} * */ function getInlineEditors(hostEditor) { var inlineEditors = []; hostEditor.getInlineWidgets().forEach(function (widget) { if (widget instanceof InlineTextEditor) { inlineEditors = inlineEditors.concat(widget.editors); } }); return inlineEditors; } /** * @private * Creates a new "full-size" (not inline) Editor for the given Document, and sets it as the * Document's master backing editor. The editor is not yet visible; to show it, use * DocumentManager.setCurrentDocument(). * Semi-private: should only be called within this module or by Document. * @param {!Document} document Document whose main/full Editor to create */ function _createFullEditorForDocument(document) { // Create editor; make it initially invisible var container = _editorHolder.get(0); var editor = _createEditorForDocument(document, true, container); editor.setVisible(false); } /** Returns the visible full-size Editor corresponding to DocumentManager.getCurrentDocument() */ function getCurrentFullEditor() { // This *should* always be equivalent to DocumentManager.getCurrentDocument()._masterEditor return _currentEditor; } /** * Creates a new inline Editor instance for the given Document. The editor's mode is inferred * based on the file extension. The editor is not yet visible or attached to a host editor. * @param {!Document} doc Document for the Editor's content * @param {?{startLine:Number, endLine:Number}} range If specified, all lines outside the given * range are hidden from the editor. Range is inclusive. Line numbers start at 0. * @param {HTMLDivContainer} inlineContent * @param {function(inlineWidget)} closeThisInline * * @return {{content:DOMElement, editor:Editor}} */ function createInlineEditorForDocument(doc, range, inlineContent, additionalKeys) { // Create the Editor var inlineEditor = _createEditorForDocument(doc, false, inlineContent, range, additionalKeys); $(exports).triggerHandler("focusedEditorChange", inlineEditor); return { content: inlineContent, editor: inlineEditor }; } /** * Disposes the given Document's full-size editor if the doc is no longer "open" from the user's * standpoint - not in the working set and not currentDocument). * * Destroying the full-size editor releases ONE ref to the Document; if inline editors or other * UI elements are still referencing the Document it will still be 'open' (kept alive) from * DocumentManager's standpoint. However, destroying the full-size editor does remove the backing * "master" editor from the Document, rendering it immutable until either inline-editor edits or * currentDocument change triggers _createFullEditorForDocument() full-size editor again. * * In certain edge cases, this is called directly by DocumentManager; see _gcDocuments() for details. * * @param {!Document} document Document whose "master" editor we may destroy */ function _destroyEditorIfUnneeded(document) { var editor = document._masterEditor; if (!editor) { return; } // If outgoing editor is no longer needed, dispose it var isCurrentDocument = (DocumentManager.getCurrentDocument() === document); var isInWorkingSet = (DocumentManager.findInWorkingSet(document.file.fullPath) !== -1); if (!isCurrentDocument && !isInWorkingSet) { // Destroy the editor widget (which un-refs the Document and reverts it to read-only mode) editor.destroy(); // Our callers should really ensure this, but just for safety... if (_currentEditor === editor) { _currentEditorsDocument = null; _currentEditor = null; } } } /** Focus the currently visible full-size editor. If no editor visible, does nothing. */ function focusEditor() { if (_currentEditor) { _currentEditor.focus(); } } /** * Resize the editor. This should only be called if the contents of the editor holder are changed * or if the height of the editor holder changes (except for overall window resizes, which are * already taken care of automatically). * @see #_updateEditorSize() */ function resizeEditor() { if (_currentEditor) { $(_currentEditor.getScrollerElement()).height(_editorHolder.height()); _currentEditor.refresh(); } } /** * NJ's editor-resizing fix. Whenever the window resizes, we immediately adjust the editor's * height. * @see #resizeEditor() */ function _updateEditorSize() { // The editor itself will call refresh() when it gets the window resize event. if (_currentEditor) { $(_currentEditor.getScrollerElement()).height(_editorHolder.height()); } } /** * @private */ function _doShow(document) { // Show new editor _currentEditorsDocument = document; _currentEditor = document._masterEditor; _currentEditor.setVisible(true); // Window may have been resized since last time editor was visible, so kick it now resizeEditor(); $(exports).triggerHandler("focusedEditorChange", _currentEditor); } /** * Make the given document's editor visible in the UI, hiding whatever was * visible before. Creates a new editor if none is assigned. * @param {!Document} document */ function _showEditor(document) { // Hide whatever was visible before if (!_currentEditor) { $("#not-editor").css("display", "none"); } else { _currentEditor.setVisible(false); _destroyEditorIfUnneeded(_currentEditorsDocument); } // Ensure a main editor exists for this document to show in the UI if (!document._masterEditor) { // Editor doesn't exist: populate a new Editor with the text _createFullEditorForDocument(document); } _doShow(document); } /** Hide the currently visible editor and show a placeholder UI in its place */ function _showNoEditor() { if (_currentEditor) { _currentEditor.setVisible(false); _destroyEditorIfUnneeded(_currentEditorsDocument); _currentEditorsDocument = null; _currentEditor = null; $("#not-editor").css("display", ""); $(exports).triggerHandler("focusedEditorChange", _currentEditor); } } /** Handles changes to DocumentManager.getCurrentDocument() */ function _onCurrentDocumentChange() { var doc = DocumentManager.getCurrentDocument(), container = _editorHolder.get(0); var perfTimerName = PerfUtils.markStart("EditorManager._onCurrentDocumentChange():\t" + (!doc || doc.file.fullPath)); // Remove scroller-shadow from the current editor if (_currentEditor) { ViewUtils.removeScrollerShadow(container, _currentEditor); } // Update the UI to show the right editor (or nothing), and also dispose old editor if no // longer needed. if (doc) { _showEditor(doc); ViewUtils.addScrollerShadow(container, _currentEditor); } else { _showNoEditor(); } PerfUtils.addMeasurement(perfTimerName); } /** Handles removals from DocumentManager's working set list */ function _onWorkingSetRemove(event, removedFile) { // There's one case where an editor should be disposed even though the current document // didn't change: removing a document from the working set (via the "X" button). (This may // also cover the case where the document WAS current, if the editor-swap happens before the // removal from the working set. var doc = DocumentManager.getOpenDocumentForPath(removedFile.fullPath); if (doc) { _destroyEditorIfUnneeded(doc); } // else, file was listed in working set but never shown in the editor - ignore } function _onWorkingSetRemoveList(event, removedFiles) { removedFiles.forEach(function (removedFile) { _onWorkingSetRemove(event, removedFile); }); } // Note: there are several paths that can lead to an editor getting destroyed // - file was in working set, but not in current editor; then closed (via working set "X" button) // --> handled by _onWorkingSetRemove() // - file was in current editor, but not in working set; then navigated away from // --> handled by _onCurrentDocumentChange() // - file was in current editor, but not in working set; then closed (via File > Close) (and thus // implicitly navigated away from) // --> handled by _onCurrentDocumentChange() // - file was in current editor AND in working set; then closed (via File > Close OR working set // "X" button) (and thus implicitly navigated away from) // --> handled by _onWorkingSetRemove() currently, but could be _onCurrentDocumentChange() // just as easily (depends on the order of events coming from DocumentManager) /** * Designates the DOM node that will contain the currently active editor instance. EditorManager * will own the content of this DOM node. * @param {!jQueryObject} holder */ function setEditorHolder(holder) { if (_currentEditor) { throw new Error("Cannot change editor area after an editor has already been created!"); } _editorHolder = holder; } /** * Returns the currently focused inline widget. * @returns {?{widget:!InlineTextEditor, editor:!Editor}} */ function getFocusedInlineWidget() { var result = null; if (_currentEditor) { _currentEditor.getInlineWidgets().forEach(function (widget) { if (widget instanceof InlineTextEditor) { widget.editors.forEach(function (editor) { if (editor.hasFocus()) { result = { widget: widget, editor: editor }; } }); } }); } return result; } /** * Returns the currently focused editor instance (full-sized OR inline editor). * @returns {Editor} */ function getFocusedEditor() { if (_currentEditor) { // See if any inlines have focus var focusedInline = getFocusedInlineWidget(); if (focusedInline) { return focusedInline.editor; } // otherwise, see if full-sized editor has focus if (_currentEditor.hasFocus()) { return _currentEditor; } } return null; } /** * Toggle Quick Edit command handler * @return {!Promise} A promise resolved with true if an inline editor * is opened or false when closed. The promise is rejected if there * is no current editor or an inline editor is not created. */ function _toggleQuickEdit() { var result = new $.Deferred(); if (_currentEditor) { var inlineWidget = null, focusedWidgetResult = getFocusedInlineWidget(); if (focusedWidgetResult) { inlineWidget = focusedWidgetResult.widget; } if (inlineWidget) { // an inline widget's editor has focus, so close it PerfUtils.markStart(PerfUtils.INLINE_EDITOR_CLOSE); inlineWidget.close(); PerfUtils.addMeasurement(PerfUtils.INLINE_EDITOR_CLOSE); // return a resolved promise to CommandManager result.resolve(false); } else { // main editor has focus, so create an inline editor _openInlineWidget(_currentEditor).done(function () { result.resolve(true); }).fail(function () { result.reject(); }); } } else { // Can not open an inline editor without a host editor result.reject(); } return result.promise(); } CommandManager.register(Strings.CMD_TOGGLE_QUICK_EDIT, Commands.TOGGLE_QUICK_EDIT, _toggleQuickEdit); // Initialize: register listeners $(DocumentManager).on("currentDocumentChange", _onCurrentDocumentChange); $(DocumentManager).on("workingSetRemove", _onWorkingSetRemove); $(DocumentManager).on("workingSetRemoveList", _onWorkingSetRemoveList); // Add this as a capture handler so we're guaranteed to run it before the editor does its own // refresh on resize. window.addEventListener("resize", _updateEditorSize, true); // For unit tests exports._openInlineWidget = _openInlineWidget; // Define public API exports.setEditorHolder = setEditorHolder; exports.getCurrentFullEditor = getCurrentFullEditor; exports.createInlineEditorForDocument = createInlineEditorForDocument; exports._createFullEditorForDocument = _createFullEditorForDocument; exports._destroyEditorIfUnneeded = _destroyEditorIfUnneeded; exports.focusEditor = focusEditor; exports.getFocusedEditor = getFocusedEditor; exports.getFocusedInlineWidget = getFocusedInlineWidget; exports.resizeEditor = resizeEditor; exports.registerInlineEditProvider = registerInlineEditProvider; exports.getInlineEditors = getInlineEditors; exports.closeInlineWidget = closeInlineWidget; exports.mergeExtraKeys = mergeExtraKeys; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window, MouseEvent */ define('command/Menus',['require','exports','module','command/Commands','command/KeyBindingManager','editor/EditorManager','strings','utils/StringUtils','command/CommandManager','widgets/PopUpManager'],function (require, exports, module) { // Load dependent modules var Commands = require("command/Commands"), KeyBindingManager = require("command/KeyBindingManager"), EditorManager = require("editor/EditorManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), CommandManager = require("command/CommandManager"), PopUpManager = require("widgets/PopUpManager"); /** * Brackets Application Menu Constants * @enum {string} */ var AppMenuBar = { FILE_MENU: "file-menu", EDIT_MENU: "edit-menu", VIEW_MENU : "view-menu", NAVIGATE_MENU: "navigate-menu", DEBUG_MENU: "debug-menu" }; /** * Brackets Context Menu Constants * @enum {string} */ var ContextMenuIds = { EDITOR_MENU: "editor-context-menu", INLINE_EDITOR_MENU: "inline-editor-context-menu", PROJECT_MENU: "project-context-menu" }; /** * Brackets Application Menu Section Constants * It is preferred that plug-ins specify the location of new MenuItems * in terms of a menu section rather than a specific MenuItem. This provides * looser coupling to Bracket's internal MenuItems and makes menu organization * more semantic. * Use these constants as the "relativeID" parameter when calling addMenuItem() and * specify a position of FIRST_IN_SECTION or LAST_IN_SECTION. * * Menu sections are denoted by dividers or the beginning/end of a menu */ var MenuSection = { // Menu Section Command ID to mark the section FILE_OPEN_CLOSE_COMMANDS: {sectionMarker: Commands.FILE_NEW}, FILE_SAVE_COMMANDS: {sectionMarker: Commands.FILE_SAVE}, FILE_LIVE: {sectionMarker: Commands.FILE_LIVE_FILE_PREVIEW}, EDIT_SELECTION_COMMANDS: {sectionMarker: Commands.EDIT_SELECT_ALL}, EDIT_FIND: {sectionMarker: Commands.EDIT_FIND}, EDIT_REPLACE_COMMANDS: {sectionMarker: Commands.EDIT_REPLACE}, EDIT_MODIFY_SELECTION: {sectionMarker: Commands.EDIT_INDENT}, VIEW_HIDESHOW_COMMANDS: {sectionMarker: Commands.VIEW_HIDE_SIDEBAR}, VIEW_FONTSIZE_COMMANDS: {sectionMarker: Commands.VIEW_INCREASE_FONT_SIZE}, NAVIGATE_GOTO_COMMANDS: {sectionMarker: Commands.NAVIGATE_QUICK_OPEN}, NAVIGATE_QUICK_EDIT_COMMANDS: {sectionMarker: Commands.TOGGLE_QUICK_EDIT} }; /** * Insertion position constants * Used by addMenu(), addMenuItem(), and addSubMenu() to * specify the relative position of a newly created menu object * @enum {string} */ var BEFORE = "before"; var AFTER = "after"; var FIRST = "first"; var LAST = "last"; var FIRST_IN_SECTION = "firstInSection"; var LAST_IN_SECTION = "lastInSection"; /** * Other constants */ var DIVIDER = "---"; /** * Maps menuID's to Menu objects * @type {Object.<string, Menu>} */ var menuMap = {}; /** * Maps contextMenuID's to ContextMenu objects * @type {Object.<string, ContextMenu>} */ var contextMenuMap = {}; /** * Maps menuItemID's to MenuItem objects * @type {Object.<string, MenuItem>} */ var menuItemMap = {}; /** * Retrieves the Menu object for the corresponding id. * @param {string} id * @return {Menu} */ function getMenu(id) { return menuMap[id]; } /** * Retrieves the ContextMenu object for the corresponding id. * @param {string} id * @return {ContextMenu} */ function getContextMenu(id) { return contextMenuMap[id]; } /** * Retrieves the MenuItem object for the corresponding id. * @param {string} id * @return {MenuItem} */ function getMenuItem(id) { return menuItemMap[id]; } function _getHTMLMenu(id) { return $("#" + StringUtils.jQueryIdEscape(id)).get(0); } function _getHTMLMenuItem(id) { return $("#" + StringUtils.jQueryIdEscape(id)).get(0); } function _addKeyBindingToMenuItem($menuItem, key, displayKey) { var $shortcut = $menuItem.find(".menu-shortcut"); if ($shortcut.length === 0) { $shortcut = $("<span class='menu-shortcut' />"); $menuItem.append($shortcut); } $shortcut.data("key", key); $shortcut.text(KeyBindingManager.formatKeyDescriptor(displayKey)); } function _addExistingKeyBinding(menuItem) { var bindings = KeyBindingManager.getKeyBindings(menuItem.getCommand().getID()), binding = null; if (bindings.length > 0) { // add the latest key binding binding = bindings[bindings.length - 1]; _addKeyBindingToMenuItem($(_getHTMLMenuItem(menuItem.id)), binding.key, binding.displayKey); } return binding; } /** NOT IMPLEMENTED * Removes MenuItem * * TODO Question: for convenience should API provide a way to remove related * keybindings and Command object? */ // function removeMenuItem(id) { // NOT IMPLEMENTED // } var _menuDividerIDCount = 1; function _getNextMenuItemDividerID() { return "brackets-menuDivider-" + _menuDividerIDCount++; } // Help function for inserting elements into a list function _insertInList($list, $element, position, $relativeElement) { // Determine where to insert. Default is LAST. var inserted = false; if (position) { // Adjust relative position for menu section positions since $relativeElement // has already been resolved by _getRelativeMenuItem() to a menuItem if (position === FIRST_IN_SECTION) { position = BEFORE; } else if (position === LAST_IN_SECTION) { position = AFTER; } if (position === FIRST) { $list.prepend($element); inserted = true; } else if ($relativeElement && $relativeElement.length > 0) { if (position === AFTER) { $relativeElement.after($element); inserted = true; } else if (position === BEFORE) { $relativeElement.before($element); inserted = true; } } } // Default to LAST if (!inserted) { $list.append($element); } } /** * @constructor * @private * * MenuItem represents a single menu item that executes a Command or a menu divider. MenuItems * may have a sub-menu. A MenuItem may correspond to an HTML-based * menu item or a native menu item if Brackets is running in a native application shell * * Since MenuItems may have a native implementation clients should create MenuItems through * addMenuItem() and should NOT construct a MenuItem object directly. * Clients should also not access HTML content of a menu directly and instead use * the MenuItem API to query and modify menus items. * * MenuItems are views on to Command objects so modify the underlying Command to modify the * name, enabled, and checked state of a MenuItem. The MenuItem will update automatically * * @param {string} id * @param {string|Command} command - the Command this MenuItem will reflect. * Use DIVIDER to specify a menu divider */ function MenuItem(id, command) { this.id = id; this.isDivider = (command === DIVIDER); if (!this.isDivider) { // Bind event handlers this._enabledChanged = this._enabledChanged.bind(this); this._checkedChanged = this._checkedChanged.bind(this); this._nameChanged = this._nameChanged.bind(this); this._keyBindingAdded = this._keyBindingAdded.bind(this); this._keyBindingRemoved = this._keyBindingRemoved.bind(this); this._command = command; $(this._command) .on("enabledStateChange", this._enabledChanged) .on("checkedStateChange", this._checkedChanged) .on("nameChange", this._nameChanged) .on("keyBindingAdded", this._keyBindingAdded) .on("keyBindingRemoved", this._keyBindingRemoved); } } /** * @constructor * @private * * Menu represents a top-level menu in the menu bar. A Menu may correspond to an HTML-based * menu or a native menu if Brackets is running in a native application shell. * * Since menus may have a native implementation clients should create Menus through * addMenu() and should NOT construct a Menu object directly. * Clients should also not access HTML content of a menu directly and instead use * the Menu API to query and modify menus. * */ function Menu(id) { this.id = id; } Menu.prototype._getMenuItemId = function (commandId) { return (this.id + "-" + commandId); }; /** * Determine MenuItem in this Menu, that has the specified command * * @param {Command} command - the command to search for. * @return {?HTMLLIElement} menu item list element */ Menu.prototype._getMenuItemForCommand = function (command) { if (!command) { return null; } var foundMenuItem = menuItemMap[this._getMenuItemId(command.getID())]; if (!foundMenuItem) { return null; } return $(_getHTMLMenuItem(foundMenuItem.id)).closest("li"); }; /** * Determine relative MenuItem * * @param {?string} relativeID - id of command (future: sub-menu). * @param {?string} position - only needed when relativeID is a MenuSection * @return {?HTMLLIElement} menu item list element */ Menu.prototype._getRelativeMenuItem = function (relativeID, position) { var $relativeElement, key, menuItem, map, foundMenuItem; if (relativeID) { if (position === FIRST_IN_SECTION || position === LAST_IN_SECTION) { if (!relativeID.hasOwnProperty("sectionMarker")) { console.log("Bad Parameter in _getRelativeMenuItem(): relativeID must be a MenuSection when position refers to a menu section"); return null; } // Determine the $relativeElement by traversing the sibling list and // stop at the first divider found // TODO: simplify using nextUntil()/prevUntil() var $sectionMarker = this._getMenuItemForCommand(CommandManager.get(relativeID.sectionMarker)); if (!$sectionMarker) { console.log("_getRelativeMenuItem(): MenuSection " + relativeID.sectionMarker + " not found in Menu " + this.id); return null; } var $listElem = $sectionMarker; $relativeElement = $listElem; while (true) { $listElem = (position === FIRST_IN_SECTION ? $listElem.prev() : $listElem.next()); if ($listElem.length === 0) { break; } else if ($listElem.find(".divider").length > 0) { break; } else { $relativeElement = $listElem; } } } else { if (relativeID.hasOwnProperty("sectionMarker")) { console.log("Bad Parameter in _getRelativeMenuItem(): if relativeID is a MenuSection, position must be FIRST_IN_SECTION or LAST_IN_SECTION"); return null; } // handle FIRST, LAST, BEFORE, & AFTER var command = CommandManager.get(relativeID); if (command) { // Lookup Command for this Command id // Find MenuItem that has this command $relativeElement = this._getMenuItemForCommand(command); } if (!$relativeElement) { console.log("_getRelativeMenuItem(): MenuItem with Command id " + relativeID + " not found in Menu " + this.id); return null; } } return $relativeElement; } else if (position && position !== FIRST && position !== LAST) { console.log("Bad Parameter in _getRelativeMenuItem(): relative position specified with no relativeID"); return null; } return $relativeElement; }; /** * Adds a new menu item with the specified id and display text. The insertion position is * specified via the relativeID and position arguments which describe a position * relative to another MenuItem or MenuGroup. It is preferred that plug-ins * insert new MenuItems relative to a menu section rather than a specific * MenuItem (see Menu Section Constants). * * TODO: Sub-menus are not yet supported, but when they are implemented this API will * allow adding new MenuItems to sub-menus as well. * * Note, keyBindings are bound to Command objects not MenuItems. The provided keyBindings * will be bound to the supplied Command object rather than the MenuItem. * * @param {!string | Command} command - the command the menu will execute. * Pass Menus.DIVIDER for a menu divider, or just call addMenuDivider() instead. * @param {?string | Array.<{key: string, platform: string}>} keyBindings - register one * one or more key bindings to associate with the supplied command. * @param {?string} position - constant defining the position of new the MenuItem relative * to other MenuItems. Default is LAST. (see Insertion position constants). * @param {?string} relativeID - id of command or menu section (future: sub-menu) that * the new menuItem will be positioned relative to. Required for all position constants * except FIRST and LAST. * * @return {MenuItem} the newly created MenuItem */ Menu.prototype.addMenuItem = function (command, keyBindings, position, relativeID) { var id, $menuItem, $link, menuItem, name, commandID; if (!command) { throw new Error("addMenuItem(): missing required parameters: command"); } if (typeof (command) === "string") { if (command === DIVIDER) { name = DIVIDER; commandID = _getNextMenuItemDividerID(); } else { commandID = command; command = CommandManager.get(commandID); if (!command) { throw new Error("addMenuItem(): commandID not found: " + commandID); } name = command.getName(); } } else { commandID = command.getID(); } // Internal id is the a composite of the parent menu id and the command id. id = this._getMenuItemId(commandID); if (menuItemMap[id]) { console.log("MenuItem added with same id of existing MenuItem: " + id); return null; } // create MenuItem menuItem = new MenuItem(id, command); menuItemMap[id] = menuItem; // create MenuItem DOM if (name === DIVIDER) { $menuItem = $("<li><hr class='divider' /></li>"); } else { // Create the HTML Menu $menuItem = $("<li><a href='#' id='" + id + "'> <span class='menu-name'></span></a></li>"); $menuItem.on("click", function () { menuItem._command.execute(); }); } // Insert menu item var $relativeElement = this._getRelativeMenuItem(relativeID, position); _insertInList($("li#" + StringUtils.jQueryIdEscape(this.id) + " > ul.dropdown-menu"), $menuItem, position, $relativeElement); // Initialize MenuItem state if (!menuItem.isDivider) { if (keyBindings) { // Add key bindings. The MenuItem listens to the Command object to update MenuItem DOM with shortcuts. if (!Array.isArray(keyBindings)) { keyBindings = [keyBindings]; } // Note that keyBindings passed during MenuItem creation take precedent over any existing key bindings KeyBindingManager.addBinding(commandID, keyBindings); } else { // Look for existing key bindings _addExistingKeyBinding(menuItem, commandID); } menuItem._checkedChanged(); menuItem._enabledChanged(); menuItem._nameChanged(); } return menuItem; }; /** * Inserts divider item in menu. * @param {?string} position - constant defining the position of new the divider relative * to other MenuItems. Default is LAST. (see Insertion position constants). * @param {?string} relativeID - id of menuItem, sub-menu, or menu section that the new * divider will be positioned relative to. Required for all position constants * except FIRST and LAST * * @return {MenuItem} the newly created divider */ Menu.prototype.addMenuDivider = function (position, relativeID) { return this.addMenuItem(DIVIDER, "", position, relativeID); }; /** * NOT IMPLEMENTED * Alternative JSON based API to addMenuItem() * * All properties are required unless noted as optional. * * @param { Array.<{ * id: string, * command: string | Command, * ?bindings: string | Array.<{key: string, platform: string}>, * }>} jsonStr * } * @param {?string} position - constant defining the position of new the MenuItem relative * to other MenuItems. Default is LAST. (see Insertion position constants). * @param {?string} relativeID - id of menuItem, sub-menu, or menu section that the new * menuItem will be positioned relative to. Required when position is * AFTER or BEFORE, ignored when position is FIRST or LAST. * * @return {MenuItem} the newly created MenuItem */ // Menu.prototype.createMenuItemsFromJSON = function (jsonStr, position, relativeID) { // NOT IMPLEMENTED // }; /** * NOT IMPLEMENTED * @param {!string} text displayed in menu item * @param {!string} id * @param {?string} position - constant defining the position of new the MenuItem relative * to other MenuItems. Default is LAST. (see Insertion position constants) * @param {?string} relativeID - id of menuItem, sub-menu, or menu section that the new * menuItem will be positioned relative to. Required when position is * AFTER or BEFORE, ignored when position is FIRST or LAST. * * @return {MenuItem} newly created menuItem for sub-menu */ // MenuItem.prototype.createSubMenu = function (text, id, position, relativeID) { // NOT IMPLEMENTED // }; /** * Gets the Command associated with a MenuItem * @return {Command} */ MenuItem.prototype.getCommand = function () { return this._command; }; /** * NOT IMPLEMENTED * Returns the parent MenuItem if the menu item is a sub-menu, returns null otherwise. * @return {MenuItem} */ // MenuItem.prototype.getParentMenuItem = function () { // NOT IMPLEMENTED; // }; /** * Returns the parent Menu for this MenuItem * @return {Menu} */ MenuItem.prototype.getParentMenu = function () { var parent = $(_getHTMLMenuItem(this.id)).parents(".dropdown").get(0); if (!parent) { return null; } return getMenu(parent.id); }; /** * Synchronizes MenuItem checked state with underlying Command checked state */ MenuItem.prototype._checkedChanged = function () { var checked = this._command.getChecked(); // Note, checked can also be undefined, so we explicitly check // for truthiness and don't use toggleClass(). if (checked) { $(_getHTMLMenuItem(this.id)).addClass("checked"); } else { $(_getHTMLMenuItem(this.id)).removeClass("checked"); } }; /** * Synchronizes MenuItem enabled state with underlying Command enabled state */ MenuItem.prototype._enabledChanged = function () { $(_getHTMLMenuItem(this.id)).toggleClass("disabled", !this._command.getEnabled()); }; /** * Synchronizes MenuItem name with underlying Command name */ MenuItem.prototype._nameChanged = function () { $(_getHTMLMenuItem(this.id)).find(".menu-name").text(this._command.getName()); }; /** * @private * Updates MenuItem DOM with a keyboard shortcut label */ MenuItem.prototype._keyBindingAdded = function (event, keyBinding) { _addKeyBindingToMenuItem($(_getHTMLMenuItem(this.id)), keyBinding.key, keyBinding.displayKey); }; /** * @private * Updates MenuItem DOM to remove keyboard shortcut label */ MenuItem.prototype._keyBindingRemoved = function (event, keyBinding) { var $shortcut = $(_getHTMLMenuItem(this.id)).find(".menu-shortcut"); if ($shortcut.length > 0 && $shortcut.data("key") === keyBinding.key) { // check for any other bindings if (_addExistingKeyBinding(this) === null) { $shortcut.empty(); } } }; /** * Closes all menus that are open */ function closeAll() { $(".dropdown").removeClass("open"); } /** * Adds a top-level menu to the application menu bar which may be native or HTML-based. * * @param {!string} name - display text for menu * @param {!string} id - unique identifier for a menu. * Core Menus in Brackets use a simple title as an id, for example "file-menu". * Extensions should use the following format: "author.myextension.mymenuname". * @param {?string} position - constant defining the position of new the Menu relative * to other Menus. Default is LAST (see Insertion position constants). * * @param {?string} relativeID - id of Menu the new Menu will be positioned relative to. Required * when position is AFTER or BEFORE, ignored when position is FIRST or LAST * * @return {?Menu} the newly created Menu */ function addMenu(name, id, position, relativeID) { name = StringUtils.htmlEscape(name); var $menubar = $("#main-toolbar .nav"), menu; if (!name || !id) { throw new Error("call to addMenu() is missing required parameters"); } // Guard against duplicate menu ids if (menuMap[id]) { console.log("Menu added with same name and id of existing Menu: " + id); return null; } menu = new Menu(id); menuMap[id] = menu; var $toggle = $("<a href='#' class='dropdown-toggle'>" + name + "</a>"), $popUp = $("<ul class='dropdown-menu'></ul>"), $newMenu = $("<li class='dropdown' id='" + id + "'></li>").append($toggle).append($popUp); // Insert menu var $relativeElement = relativeID && $(_getHTMLMenu(relativeID)); _insertInList($menubar, $newMenu, position, $relativeElement); // Install ESC key handling PopUpManager.addPopUp($popUp, closeAll, false); // todo error handling return menu; } /** * @constructor * @extends {Menu} * * Represents a context menu that can open at a specific location in the UI. * * Clients should not create this object directly and should instead use registerContextMenu() * to create new ContextMenu objects. * * Context menus in brackets may be HTML-based or native so clients should not reach into * the HTML and should instead manipulate ContextMenus through the API. * * Events: * beforeContextMenuOpen * contextMenuClose * */ function ContextMenu(id) { this.id = id; this.menu = new Menu(id); var $newMenu = $("<li class='dropdown context-menu' id='" + StringUtils.jQueryIdEscape(id) + "'></li>"), $popUp = $("<ul class='dropdown-menu'></ul>"), $toggle = $("<a href='#' class='dropdown-toggle'></a>").hide(); // assemble the menu fragments $newMenu.append($toggle).append($popUp); // insert into DOM $("#context-menu-bar > ul").append($newMenu); var self = this; PopUpManager.addPopUp($popUp, function () { self.close(); }, false); } ContextMenu.prototype = new Menu(); ContextMenu.prototype.constructor = ContextMenu; ContextMenu.prototype.parentClass = Menu.prototype; /** * Displays the ContextMenu at the specified location and dispatches the * "beforeContextMenuOpen" event.The menu location may be adjusted to prevent * clipping by the browser window. All other menus and ContextMenus will be closed * bofore a new menu is shown. * * @param {MouseEvent | {pageX:number, pageY:number}} mouseOrLocation - pass a MouseEvent * to display the menu near the mouse or pass in an object with page x/y coordinates * for a specific location. */ ContextMenu.prototype.open = function (mouseOrLocation) { if (!mouseOrLocation || !mouseOrLocation.hasOwnProperty("pageX") || !mouseOrLocation.hasOwnProperty("pageY")) { throw new Error("ContextMenu open(): missing required parameter"); } var $window = $(window), escapedId = StringUtils.jQueryIdEscape(this.id), $menuAnchor = $("#" + escapedId), $menuWindow = $("#" + escapedId + " > ul"), posTop = mouseOrLocation.pageY, posLeft = mouseOrLocation.pageX; // only show context menu if it has menu items if ($menuWindow.children().length <= 0) { return; } $(this).triggerHandler("beforeContextMenuOpen"); // close all other dropdowns closeAll(); // adjust positioning so menu is not clipped off bottom or right var bottomOverhang = posTop + 25 + $menuWindow.height() - $window.height(); if (bottomOverhang > 0) { posTop = Math.max(0, posTop - bottomOverhang); } posTop -= 30; // shift top for hidden parent element posLeft += 5; var rightOverhang = posLeft + $menuWindow.width() - $window.width(); if (rightOverhang > 0) { posLeft = Math.max(0, posLeft - rightOverhang); } // open the context menu at final location $menuAnchor.addClass("open") .css({"left": posLeft, "top": posTop}); }; /** * Closes the context menu and dispatches the "contextMenuClose" event. */ ContextMenu.prototype.close = function () { $("#" + StringUtils.jQueryIdEscape(this.id)).removeClass("open"); $(this).triggerHandler("contextMenuClose"); }; /** * Registers new context menu with Brackets. * Extensions should generally use the predefined context menus built into Brackets. Use this * API to add a new context menu to UI that is specific to an extension. * * After registering a new context menu clients should: * - use addMenuItem() to add items to the context menu * - call open() to show the context menu. * For example: * $("#my_ID").contextmenu(function (e) { * if (e.which === 3) { * my_cmenu.open(e); * } * }); * * To make menu items be contextual to things like selection, listen for the "beforeContextMenuOpen" * to make changes to Command objects before the context menu is shown. MenuItems are views of * Commands, which control a MenuItem's name, enabled state, and checked state. * * @param {string} id - unique identifier for context menu. * Core context menus in Brackets use a simple title as an id. * Extensions should use the following format: "author.myextension.mycontextmenu name" * @return {?ContextMenu} the newly created context menu */ function registerContextMenu(id) { if (!id) { throw new Error("call to registerContextMenu() is missing required parameters"); } // Guard against duplicate menu ids if (contextMenuMap[id]) { console.log("Context Menu added with same name and id of existing Context Menu: " + id); return null; } var cmenu = new ContextMenu(id); contextMenuMap[id] = cmenu; return cmenu; } /** NOT IMPLEMENTED * Removes Menu */ // function removeMenu(id) { // NOT IMPLEMENTED // } function init() { /* * File menu */ var menu; menu = addMenu(Strings.FILE_MENU, AppMenuBar.FILE_MENU); menu.addMenuItem(Commands.FILE_NEW, "Ctrl-N"); menu.addMenuItem(Commands.FILE_OPEN, "Ctrl-O"); menu.addMenuItem(Commands.FILE_OPEN_FOLDER); menu.addMenuItem(Commands.FILE_CLOSE, "Ctrl-W"); menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_SAVE, "Ctrl-S"); menu.addMenuItem(Commands.FILE_SAVE_ALL, "Ctrl-Alt-S"); menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW, "Ctrl-Alt-P"); menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_QUIT, "Ctrl-Q"); /* * Edit menu */ menu = addMenu(Strings.EDIT_MENU, AppMenuBar.EDIT_MENU); menu.addMenuItem(Commands.EDIT_SELECT_ALL, "Ctrl-A"); menu.addMenuDivider(); menu.addMenuItem(Commands.EDIT_FIND, "Ctrl-F"); menu.addMenuItem(Commands.EDIT_FIND_IN_FILES, "Ctrl-Shift-F"); menu.addMenuItem(Commands.EDIT_FIND_NEXT, [{key: "F3", platform: "win"}, {key: "Cmd-G", platform: "mac"}]); menu.addMenuItem(Commands.EDIT_FIND_PREVIOUS, [{key: "Shift-F3", platform: "win"}, {key: "Cmd-Shift-G", platform: "mac"}]); menu.addMenuDivider(); menu.addMenuItem(Commands.EDIT_REPLACE, [{key: "Ctrl-H", platform: "win"}, {key: "Cmd-Alt-F", platform: "mac"}]); menu.addMenuDivider(); menu.addMenuItem(Commands.EDIT_INDENT, [{key: "Indent", displayKey: "Tab"}]); menu.addMenuItem(Commands.EDIT_UNINDENT, [{key: "Unindent", displayKey: "Shift-Tab"}]); menu.addMenuItem(Commands.EDIT_DUPLICATE, "Ctrl-D"); menu.addMenuItem(Commands.EDIT_LINE_UP, [{key: "Ctrl-Shift-Up", displayKey: "Ctrl-Shift-\u2191", platform: "win"}, {key: "Cmd-Ctrl-Up", displayKey: "Cmd-Ctrl-\u2191", platform: "mac"}]); menu.addMenuItem(Commands.EDIT_LINE_DOWN, [{key: "Ctrl-Shift-Down", displayKey: "Ctrl-Shift-\u2193", platform: "win"}, {key: "Cmd-Ctrl-Down", displayKey: "Cmd-Ctrl-\u2193", platform: "mac"}]); menu.addMenuDivider(); menu.addMenuItem(Commands.EDIT_LINE_COMMENT, "Ctrl-/"); menu.addMenuDivider(); menu.addMenuItem(Commands.TOGGLE_USE_TAB_CHARS); /* * View menu */ menu = addMenu(Strings.VIEW_MENU, AppMenuBar.VIEW_MENU); menu.addMenuItem(Commands.VIEW_HIDE_SIDEBAR, "Ctrl-Shift-H"); menu.addMenuDivider(); menu.addMenuItem(Commands.VIEW_INCREASE_FONT_SIZE, [{key: "Ctrl-=", displayKey: "Ctrl-+"}]); menu.addMenuItem(Commands.VIEW_DECREASE_FONT_SIZE, [{key: "Ctrl--", displayKey: "Ctrl-\u2212"}]); menu.addMenuItem(Commands.VIEW_RESTORE_FONT_SIZE, "Ctrl-0"); menu.addMenuDivider(); menu.addMenuItem(Commands.TOGGLE_JSLINT); /* * Navigate menu */ menu = addMenu(Strings.NAVIGATE_MENU, AppMenuBar.NAVIGATE_MENU); menu.addMenuItem(Commands.NAVIGATE_QUICK_OPEN, "Ctrl-Shift-O"); menu.addMenuItem(Commands.NAVIGATE_GOTO_LINE, [{key: "Ctrl-G", platform: "win"}, {key: "Cmd-L", platform: "mac"}]); menu.addMenuItem(Commands.NAVIGATE_GOTO_DEFINITION, "Ctrl-T"); menu.addMenuDivider(); menu.addMenuItem(Commands.TOGGLE_QUICK_EDIT, "Ctrl-E"); menu.addMenuItem(Commands.QUICK_EDIT_PREV_MATCH, {key: "Alt-Up", displayKey: "Alt-\u2191"}); menu.addMenuItem(Commands.QUICK_EDIT_NEXT_MATCH, {key: "Alt-Down", displayKey: "Alt-\u2193"}); /* * Debug menu */ menu = addMenu(Strings.DEBUG_MENU, AppMenuBar.DEBUG_MENU); menu.addMenuItem(Commands.DEBUG_SHOW_DEVELOPER_TOOLS, [{key: "F12", platform: "win"}, {key: "Cmd-Opt-I", platform: "mac"}]); menu.addMenuItem(Commands.DEBUG_REFRESH_WINDOW, [{key: "F5", platform: "win"}, {key: "Cmd-R", platform: "mac"}]); menu.addMenuItem(Commands.DEBUG_NEW_BRACKETS_WINDOW); menu.addMenuItem(Commands.DEBUG_SHOW_EXT_FOLDER); menu.addMenuDivider(); menu.addMenuItem(Commands.DEBUG_SWITCH_LANGUAGE); menu.addMenuDivider(); menu.addMenuItem(Commands.DEBUG_RUN_UNIT_TESTS); menu.addMenuItem(Commands.DEBUG_SHOW_PERF_DATA); menu.addMenuDivider(); menu.addMenuItem(Commands.CHECK_FOR_UPDATE); /* * Context Menus */ var project_cmenu = registerContextMenu(ContextMenuIds.PROJECT_MENU); project_cmenu.addMenuItem(Commands.FILE_NEW); var editor_cmenu = registerContextMenu(ContextMenuIds.EDITOR_MENU); editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT); editor_cmenu.addMenuItem(Commands.EDIT_SELECT_ALL); var inline_editor_cmenu = registerContextMenu(ContextMenuIds.INLINE_EDITOR_MENU); inline_editor_cmenu.addMenuItem(Commands.TOGGLE_QUICK_EDIT); inline_editor_cmenu.addMenuItem(Commands.EDIT_SELECT_ALL); inline_editor_cmenu.addMenuDivider(); inline_editor_cmenu.addMenuItem(Commands.QUICK_EDIT_PREV_MATCH); inline_editor_cmenu.addMenuItem(Commands.QUICK_EDIT_NEXT_MATCH); /** * Context menu for code editors (both full-size and inline) * Auto selects the word the user clicks if the click does not occur over * an existing selection */ $("#editor-holder").on("contextmenu", function (e) { if ($(e.target).parents(".CodeMirror-gutter").length !== 0) { return; } // Note: on mousedown before this event, CodeMirror automatically checks mouse pos, and // if not clicking on a selection moves the cursor to click location. When triggered // from keyboard, no pre-processing occurs and the cursor/selection is left as is. var editor = EditorManager.getFocusedEditor(), inlineWidget = EditorManager.getFocusedInlineWidget(); if (editor) { // If there's just an insertion point select the word token at the cursor pos so // it's more clear what the context menu applies to. if (!editor.hasSelection()) { editor.selectWordAt(editor.getCursorPos()); // Prevent menu from overlapping text by moving it down a little // Temporarily backout this change for now to help mitigate issue #1111, // which only happens if mouse is not over context menu. Better fix // requires change to bootstrap, which is too risky for now. //e.pageY += 6; } if (inlineWidget) { inline_editor_cmenu.open(e); } else { editor_cmenu.open(e); } } }); /** * Context menu for folder tree & working set list * * TODO (#1069): change selection on right mousedown if not on something already selected */ $("#projects").on("contextmenu", function (e) { project_cmenu.open(e); }); // Prevent the browser context menu since Brackets creates a custom context menu $(window).contextmenu(function (e) { e.preventDefault(); }); /* * General menu event processing */ // Prevent clicks on top level menus and menu items from taking focus $(window.document).on("mousedown", ".dropdown", function (e) { e.preventDefault(); }); // Switch menus when the mouse enters an adjacent menu // Only open the menu if another one has already been opened // by clicking $(window.document).on("mouseenter", "#main-toolbar .dropdown", function (e) { var open = $(this).siblings(".open"); if (open.length > 0) { open.removeClass("open"); $(this).addClass("open"); } }); } // Define public API exports.init = init; exports.AppMenuBar = AppMenuBar; exports.ContextMenuIds = ContextMenuIds; exports.MenuSection = MenuSection; exports.BEFORE = BEFORE; exports.AFTER = AFTER; exports.LAST = LAST; exports.FIRST = FIRST; exports.FIRST_IN_SECTION = FIRST_IN_SECTION; exports.LAST_IN_SECTION = LAST_IN_SECTION; exports.DIVIDER = DIVIDER; exports.getMenu = getMenu; exports.getMenuItem = getMenuItem; exports.getContextMenu = getContextMenu; exports.addMenu = addMenu; exports.registerContextMenu = registerContextMenu; exports.closeAll = closeAll; exports.Menu = Menu; exports.MenuItem = MenuItem; exports.ContextMenu = ContextMenu; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Responsible for coordinating file selection between views by permitting only one view * to show the current file selection at a time. Currently, only WorkingSetView and * ProjectManager can show file selection. In general the WorkingSetView takes higher * priority until the user selects a file in the ProjectManager. * * Events dispatched: * - documentSelectionFocusChange - indicates a document change has caused the focus to * change between the working set and file tree. * * - fileViewFocusChange - indicates the selection focus has changed between the working * set and the project tree, but the document selection has NOT changed * * Current file selection rules in views: * - select a file in WorkingSetView > select in WorkingSetView * - add a file to the WorkingSetView > select in WorkingSetView * - select a file in ProjectManager > select in ProjectManager * - open a file from places other than the WorkingSetView or ProjectManager > * select file in WorkignSetView if its in the working set, otherwise select in ProjectManager */ define('project/FileViewController',['require','exports','module','document/DocumentManager','command/CommandManager','editor/EditorManager','utils/PerfUtils','command/Commands'],function (require, exports, module) { // Load dependent modules var DocumentManager = require("document/DocumentManager"), CommandManager = require("command/CommandManager"), EditorManager = require("editor/EditorManager"), PerfUtils = require("utils/PerfUtils"), Commands = require("command/Commands"); /** * Tracks whether a "currentDocumentChange" notification occured due to a call to * openAndSelectDocument. * @see FileviewController.openAndSelectDocument * @private */ var _curDocChangedDueToMe = false; var WORKING_SET_VIEW = "WorkingSetView"; var PROJECT_MANAGER = "ProjectManager"; /** * @private * @see FileViewController.getFileSelectionFocus() */ var _fileSelectionFocus = PROJECT_MANAGER; /** * Change the doc selection to the working set when ever a new file is added to the working set */ $(DocumentManager).on("workingSetAdd", function (event, addedFile) { _fileSelectionFocus = WORKING_SET_VIEW; $(exports).triggerHandler("documentSelectionFocusChange"); }); /** * Update the file selection focus when ever the current document changes */ $(DocumentManager).on("currentDocumentChange", function (event) { var perfTimerName; // The the cause of the doc change was not openAndSelectDocument, so pick the best fileSelectionFocus if (!_curDocChangedDueToMe) { var curDoc = DocumentManager.getCurrentDocument(); perfTimerName = PerfUtils.markStart("FileViewController._onCurrentDocumentChange():\t" + (!curDoc || curDoc.file.fullPath)); if (curDoc && DocumentManager.findInWorkingSet(curDoc.file.fullPath) !== -1) { _fileSelectionFocus = WORKING_SET_VIEW; } else { _fileSelectionFocus = PROJECT_MANAGER; } } $(exports).triggerHandler("documentSelectionFocusChange"); if (!_curDocChangedDueToMe) { PerfUtils.addMeasurement(perfTimerName); } }); /** * @private * @returns {$.Promise} */ function _selectCurrentDocument() { // If fullPath corresonds to the current doc being viewed then opening the file won't // trigger a currentDocumentChanged event, so we need to trigger a documentSelectionFocusChange // in this case to signify the selection focus has changed even though the current document has not. $(exports).triggerHandler("documentSelectionFocusChange"); // Ensure the editor has focus even though we didn't open a new file. EditorManager.focusEditor(); } /** * Modifies the selection focus in the project side bar. A file can either be selected * in the working set (the open files) or in the file tree, but not both. * @param {String} fileSelectionFocus - either PROJECT_MANAGER or WORKING_SET_VIEW */ function setFileViewFocus(fileSelectionFocus) { if (fileSelectionFocus !== PROJECT_MANAGER && fileSelectionFocus !== WORKING_SET_VIEW) { throw new Error("Bad parameter passed to FileViewController.setFileViewFocus"); } _fileSelectionFocus = fileSelectionFocus; $(exports).triggerHandler("fileViewFocusChange"); } /** * Opens a document if it's not open and selects the file in the UI corresponding to * fileSelectionFocus * @param {!fullPath} * @param {string} - must be either WORKING_SET_VIEW or PROJECT_MANAGER * @returns {$.Promise} */ function openAndSelectDocument(fullPath, fileSelectionFocus) { var result; if (fileSelectionFocus !== PROJECT_MANAGER && fileSelectionFocus !== WORKING_SET_VIEW) { throw new Error("Bad parameter passed to FileViewController.openAndSelectDocument"); } // Opening files are asynchronous and we want to know when this function caused a file // to open so that _fileSelectionFocus is set appropriatly. _curDocChangedDueToMe is set here // and checked in the currentDocumentChange handler _curDocChangedDueToMe = true; _fileSelectionFocus = fileSelectionFocus; // If fullPath corresonds to the current doc being viewed then opening the file won't // trigger a currentDocumentChanged event, so we need to trigger a documentSelectionFocusChange // in this case to signify the selection focus has changed even though the current document has not. var curDoc = DocumentManager.getCurrentDocument(); if (curDoc && curDoc.file.fullPath === fullPath) { _selectCurrentDocument(); result = (new $.Deferred()).resolve().promise(); } else { result = CommandManager.execute(Commands.FILE_OPEN, {fullPath: fullPath}); } // clear after notification is done result.always(function () { _curDocChangedDueToMe = false; }); return result; } /** * Opens the specified document if it's not already open, adds it to the working set, * and selects it in the WorkingSetView * @param {!fullPath} * @param {?String} selectIn - specify either WORING_SET_VIEW or PROJECT_MANAGER. * Default is WORING_SET_VIEW. * @return {!$.Promise} */ function addToWorkingSetAndSelect(fullPath, selectIn) { var result = new $.Deferred(), promise = CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, {fullPath: fullPath}); // This properly handles sending the right nofications in cases where the document // is already the current one. In that case we will want to notify with // documentSelectionFocusChange so the views change their selection promise.done(function (doc) { // FILE_ADD_TO_WORKING_SET command sets the current document. Update the // selection focus and trigger documentSelectionFocusChange event _fileSelectionFocus = selectIn || WORKING_SET_VIEW; _selectCurrentDocument(); result.resolve(doc); }).fail(function (err) { result.reject(err); }); return result.promise(); } /** * returns either WORKING_SET_VIEW or PROJECT_MANAGER * @return {!String} */ function getFileSelectionFocus() { return _fileSelectionFocus; } // Define public API exports.getFileSelectionFocus = getFileSelectionFocus; exports.openAndSelectDocument = openAndSelectDocument; exports.addToWorkingSetAndSelect = addToWorkingSetAndSelect; exports.setFileViewFocus = setFileViewFocus; exports.WORKING_SET_VIEW = WORKING_SET_VIEW; exports.PROJECT_MANAGER = PROJECT_MANAGER; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, FileError, window */ /** * ProjectManager is the model for the set of currently open project. It is responsible for * creating and updating the project tree when projects are opened and when changes occur to * the file tree. * * This module dispatches these events: * - beforeProjectClose -- before _projectRoot changes * - projectOpen -- after _projectRoot changes * * These are jQuery events, so to listen for them you do something like this: * $(ProjectManager).on("eventname", handler); */ define('project/ProjectManager',['require','exports','module','thirdparty/jstree_pre1.0_fix_1/jquery.jstree','file/NativeFileSystem','preferences/PreferencesManager','document/DocumentManager','command/CommandManager','command/Commands','widgets/Dialogs','command/Menus','utils/StringUtils','strings','project/FileViewController','utils/PerfUtils','utils/ViewUtils','file/FileUtils'],function (require, exports, module) { // Load dependent non-module scripts require("thirdparty/jstree_pre1.0_fix_1/jquery.jstree"); // Load dependent modules var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PreferencesManager = require("preferences/PreferencesManager"), DocumentManager = require("document/DocumentManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Dialogs = require("widgets/Dialogs"), Menus = require("command/Menus"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), FileViewController = require("project/FileViewController"), PerfUtils = require("utils/PerfUtils"), ViewUtils = require("utils/ViewUtils"), FileUtils = require("file/FileUtils"); /** * @private * Reference to the tree control container div. Initialized by * htmlContentLoadComplete handler * @type {jQueryObject} */ var $projectTreeContainer; /** * @private * Reference to the tree control * @type {jQueryObject} */ var _projectTree = null; /** * @private * Reference to previous selected jstree leaf node when ProjectManager had * selection focus from FileViewController. * @type {DOMElement} */ var _lastSelected = null; /** * @private * Internal flag to suppress firing of selectionChanged event. * @type {boolean} */ var _suppressSelectionChange = false; /** * @private * Reference to the tree control UL element * @type {DOMElement} */ var $projectTreeList; /** * @private * @see getProjectRoot() */ var _projectRoot = null; /** * Unique PreferencesManager clientID */ var PREFERENCES_CLIENT_ID = "com.adobe.brackets.ProjectManager"; /** * @private * @type {PreferenceStorage} */ var _prefs = null; /** * @private * Used to initialize jstree state */ var _projectInitialLoad = { previous : [], /* array of arrays containing full paths to open at each depth of the tree */ id : 0, /* incrementing id */ fullPathToIdMap : {} /* mapping of fullPath to tree node id attr */ }; /** * @private */ function _hasFileSelectionFocus() { return FileViewController.getFileSelectionFocus() === FileViewController.PROJECT_MANAGER; } /** * @private */ function _redraw(selectionChanged, reveal) { reveal = (reveal === undefined) ? true : reveal; // redraw selection if ($projectTreeList) { if (selectionChanged && !_suppressSelectionChange) { $projectTreeList.triggerHandler("selectionChanged", reveal); } // reposition the selection triangle $projectTreeContainer.triggerHandler("scroll"); // in-lieu of resize events, manually trigger contentChanged for every // FileViewController focus change. This event triggers scroll shadows // on the jstree to update. documentSelectionFocusChange fires when // a new file is added and removed (causing a new selection) from the working set _projectTree.triggerHandler("contentChanged"); } } /** * Returns the FileEntry or DirectoryEntry corresponding to the selected item, or null * if no item is selected. * * @return {?Entry} */ function getSelectedItem() { var selected = _projectTree.jstree("get_selected"); if (selected) { return selected.data("entry"); } return null; } function _fileViewFocusChange() { _redraw(true); } function _documentSelectionFocusChange() { var curDoc = DocumentManager.getCurrentDocument(); if (curDoc && _hasFileSelectionFocus()) { $("#project-files-container li").is(function (index) { var entry = $(this).data("entry"); if (entry && entry.fullPath === curDoc.file.fullPath && !_projectTree.jstree("is_selected", $(this))) { //we don't want to trigger another selection change event, so manually deselect //and select without sending out notifications _projectTree.jstree("deselect_all"); _projectTree.jstree("select_node", $(this), false); return true; } return false; }); } else if (_projectTree !== null) { _projectTree.jstree("deselect_all"); _lastSelected = null; } _redraw(true); } /** * Returns the root folder of the currently loaded project, or null if no project is open (during * startup, or running outside of app shell). * @return {DirectoryEntry} */ function getProjectRoot() { return _projectRoot; } /** * Returns true if absPath lies within the project, false otherwise. * Does not support paths containing ".." */ function isWithinProject(absPath) { return (_projectRoot && absPath.indexOf(_projectRoot.fullPath) === 0); } /** * If absPath lies within the project, returns a project-relative path. Else returns absPath * unmodified. * Does not support paths containing ".." */ function makeProjectRelativeIfPossible(absPath) { if (isWithinProject(absPath)) { return absPath.slice(_projectRoot.fullPath.length); } return absPath; } /** * Initial project path is stored in prefs, which defaults to brackets/src */ function getInitialProjectPath() { return _prefs.getValue("projectPath"); } /** * @private * Get prefs tree state lookup key for given project path. */ function _getTreeStateKey(path) { // generate unique tree state key for this project path var key = "projectTreeState_" + path; // normalize to always have slash at end if (key[key.length - 1] !== "/") { key += "/"; } return key; } /** * @private * Save ProjectManager project path and tree state. */ function _savePreferences() { // save the current project _prefs.setValue("projectPath", _projectRoot.fullPath); // save jstree state var openNodes = [], projectPathLength = _projectRoot.fullPath.length, entry, fullPath, shortPath, depth; // Query open nodes by class selector $(".jstree-open:visible").each(function (index) { entry = $(this).data("entry"); if (entry.fullPath) { fullPath = entry.fullPath; // Truncate project path prefix, remove the trailing slash shortPath = fullPath.slice(projectPathLength, -1); // Determine depth of the node by counting path separators. // Children at the root have depth of zero depth = shortPath.split("/").length - 1; // Map tree depth to list of open nodes if (openNodes[depth] === undefined) { openNodes[depth] = []; } openNodes[depth].push(fullPath); } }); // Store the open nodes by their full path and persist to storage _prefs.setValue(_getTreeStateKey(_projectRoot.fullPath), openNodes); } /** * @private */ function _forceSelection(current, target) { // select_node will force the target to be revealed. Instead, // keep the scroller position stable. var savedScrollTop = $projectTreeContainer.get(0).scrollTop; // suppress selectionChanged event from firing by jstree select_node _suppressSelectionChange = true; _projectTree.jstree("deselect_node", current); _projectTree.jstree("select_node", target, false); _suppressSelectionChange = false; $projectTreeContainer.get(0).scrollTop = savedScrollTop; _redraw(true, false); } /** * @private * Given an input to jsTree's json_data.data setting, display the data in the file tree UI * (replacing any existing file tree that was previously displayed). This input could be * raw JSON data, or it could be a dataprovider function. See jsTree docs for details: * http://www.jstree.com/documentation/json_data */ function _renderTree(treeDataProvider) { var result = new $.Deferred(), suppressToggleOpen = false; // Instantiate tree widget // (jsTree is smart enough to replace the old tree if there's already one there) $projectTreeContainer.hide(); _projectTree = $projectTreeContainer .jstree( { plugins : ["ui", "themes", "json_data", "crrm", "sort"], json_data : { data: treeDataProvider, correct_state: false }, core : { animation: 0 }, themes : { theme: "brackets", url: "styles/jsTreeTheme.css", dots: false, icons: false }, //(note: our actual jsTree theme CSS lives in brackets.less; we specify an empty .css // file because jsTree insists on loading one itself) strings : { loading : "Loading ...", new_node : "New node" }, sort : function (a, b) { if (brackets.platform === "win") { // Windows: prepend folder names with a '0' and file names with a '1' so folders are listed first var a1 = ($(a).hasClass("jstree-leaf") ? "1" : "0") + this.get_text(a).toLowerCase(), b1 = ($(b).hasClass("jstree-leaf") ? "1" : "0") + this.get_text(b).toLowerCase(); return (a1 > b1) ? 1 : -1; } else { return this.get_text(a).toLowerCase() > this.get_text(b).toLowerCase() ? 1 : -1; } } } ) .bind( "before.jstree", function (event, data) { if (data.func === "toggle_node") { // jstree will automaticaly select parent node when the parent is closed // and any descendant is selected. Prevent the select_node handler from // immediately toggling open again in this case. suppressToggleOpen = _projectTree.jstree("is_open", data.args[0]); } } ) .bind( "select_node.jstree", function (event, data) { var entry = data.rslt.obj.data("entry"); if (entry.isFile) { var openResult = FileViewController.openAndSelectDocument(entry.fullPath, FileViewController.PROJECT_MANAGER); openResult.done(function () { // update when tree display state changes _redraw(true); _lastSelected = data.rslt.obj; }).fail(function () { if (_lastSelected) { // revert this new selection and restore previous selection _forceSelection(data.rslt.obj, _lastSelected); } else { _projectTree.jstree("deselect_all"); _lastSelected = null; } }); } else { FileViewController.setFileViewFocus(FileViewController.PROJECT_MANAGER); // show selection marker on folders _redraw(true); // toggle folder open/closed // suppress if this selection was triggered by clicking the disclousre triangle if (!suppressToggleOpen) { _projectTree.jstree("toggle_node", data.rslt.obj); } } suppressToggleOpen = false; } ) .bind( "reopen.jstree", function (event, data) { // This handler fires for the initial load and subsequent // reload_nodes events. For each depth level of the tree, we open // the saved nodes by a fullPath lookup. if (_projectInitialLoad.previous.length > 0) { // load previously open nodes by increasing depth var toOpenPaths = _projectInitialLoad.previous.shift(), toOpenIds = [], node = null; // use path to lookup ID $.each(toOpenPaths, function (index, value) { node = _projectInitialLoad.fullPathToIdMap[value]; if (node) { toOpenIds.push(node); } }); // specify nodes to open and load data.inst.data.core.to_open = toOpenIds; _projectTree.jstree("reload_nodes", false); } if (_projectInitialLoad.previous.length === 0) { // resolve after all paths are opened result.resolve(); } } ) .bind( "scroll.jstree", function (e) { // close all dropdowns on scroll Menus.closeAll(); } ) .bind( "loaded.jstree open_node.jstree close_node.jstree", function (event, data) { if (event.type === "open_node") { // select the current document if it becomes visible when this folder is opened var curDoc = DocumentManager.getCurrentDocument(); if (_hasFileSelectionFocus() && curDoc) { var entry = data.rslt.obj.data("entry"); if (curDoc.file.fullPath.indexOf(entry.fullPath) === 0) { _forceSelection(data.rslt.obj, _lastSelected); } else { _redraw(true, false); } } } else if (event.type === "close_node") { // always update selection marker position when collapsing a node _redraw(true, false); } else { _redraw(false); } _savePreferences(); } ) .bind( "mousedown.jstree", function (event) { // select tree node on right-click if (event.which === 3) { var treenode = $(event.target).closest("li"); if (treenode) { var saveSuppressToggleOpen = suppressToggleOpen; // don't toggle open folders (just select) suppressToggleOpen = true; _projectTree.jstree("deselect_all"); _projectTree.jstree("select_node", treenode, false); suppressToggleOpen = saveSuppressToggleOpen; } } } ); // jstree has a default event handler for dblclick that attempts to clear the // global window selection (presumably because it doesn't want text within the tree // to be selected). This ends up messing up CodeMirror, and we don't need this anyway // since we've turned off user selection of UI text globally. So we just unbind it, // and add our own double-click handler here. // Filed this bug against jstree at https://github.com/vakata/jstree/issues/163 _projectTree.bind("init.jstree", function () { // install scroller shadows ViewUtils.addScrollerShadow(_projectTree.get(0)); _projectTree .unbind("dblclick.jstree") .bind("dblclick.jstree", function (event) { var entry = $(event.target).closest("li").data("entry"); if (entry && entry.isFile) { FileViewController.addToWorkingSetAndSelect(entry.fullPath); } }); // fire selection changed events for sidebar-selection $projectTreeList = $projectTreeContainer.find("ul"); ViewUtils.sidebarList($projectTreeContainer, "jstree-clicked", "jstree-leaf"); $projectTreeContainer.show(); }); return result.promise(); } /** @param {Entry} entry File or directory to filter */ function shouldShow(entry) { return [".git", ".svn", ".DS_Store", "Thumbs.db"].indexOf(entry.name) === -1; } /** * @private * Given an array of NativeFileSystem entries, returns a JSON array representing them in the format * required by jsTree. Saves the corresponding Entry object as metadata (which jsTree will store in * the DOM via $.data()). * * Does NOT recursively traverse the file system: folders are marked as expandable but are given no * children initially. * * @param {Array.<Entry>} entries Array of NativeFileSystem entry objects. * @return {Array} jsTree node data: array of JSON objects */ function _convertEntriesToJSON(entries) { var jsonEntryList = [], entry, entryI; for (entryI = 0; entryI < entries.length; entryI++) { entry = entries[entryI]; if (shouldShow(entry)) { var jsonEntry = { data: entry.name, attr: { id: "node" + _projectInitialLoad.id++ }, metadata: { entry: entry } }; if (entry.isDirectory) { jsonEntry.children = []; jsonEntry.state = "closed"; } // For more info on jsTree's JSON format see: http://www.jstree.com/documentation/json_data jsonEntryList.push(jsonEntry); // Map path to ID to initialize loaded and opened states _projectInitialLoad.fullPathToIdMap[entry.fullPath] = jsonEntry.attr.id; } } return jsonEntryList; } /** * @private * Called by jsTree when the user has expanded a node that has never been expanded before. We call * jsTree back asynchronously with the node's immediate children data once the subfolder is done * being fetched. * * @param {jQueryObject} treeNode jQ object for the DOM node being expanded * @param {function(Array)} jsTreeCallback jsTree callback to provide children to */ function _treeDataProvider(treeNode, jsTreeCallback) { var dirEntry, isProjectRoot = false; if (treeNode === -1) { // Special case: root of tree dirEntry = _projectRoot; isProjectRoot = true; } else { // All other nodes: the DirectoryEntry is saved as jQ data in the tree (by _convertEntriesToJSON()) dirEntry = treeNode.data("entry"); } // Fetch dirEntry's contents dirEntry.createReader().readEntries( function (entries) { var subtreeJSON = _convertEntriesToJSON(entries), wasNodeOpen = false, emptyDirectory = (subtreeJSON.length === 0); if (emptyDirectory) { if (!isProjectRoot) { wasNodeOpen = treeNode.hasClass("jstree-open"); } else { // project root is a special case, add a placeholder subtreeJSON.push({}); } } jsTreeCallback(subtreeJSON); if (!isProjectRoot && emptyDirectory) { // If the directory is empty, force it to appear as an open or closed node. // This is a workaround for issue #149 where jstree would show this node as a leaf. var classToAdd = (wasNodeOpen) ? "jstree-closed" : "jstree-open"; treeNode.removeClass("jstree-leaf jstree-closed jstree-open") .addClass(classToAdd); } }, function (error) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_LOADING_PROJECT, StringUtils.format(Strings.READ_DIRECTORY_ENTRIES_ERROR, StringUtils.htmlEscape(dirEntry.fullPath), error.code) ); } ); } /** Returns the full path to the default project folder. The path is currently the brackets src folder. * TODO: (issue #267): Brackets does not yet support operating when there is no project folder. This code will likely * not be needed when this support is added. * @private * @return {!string} fullPath reference */ function _getDefaultProjectPath() { var loadedPath = decodeURI(window.location.pathname); var bracketsSrc = loadedPath.substr(0, loadedPath.lastIndexOf("/")); bracketsSrc = FileUtils.convertToNativePath(bracketsSrc); return bracketsSrc; } /** * Loads the given folder as a project. Normally, you would call openProject() instead to let the * user choose a folder. * * @param {string} rootPath Absolute path to the root folder of the project. * If rootPath is undefined or null, the last open project will be restored. * @return {$.Promise} A promise object that will be resolved when the * project is loaded and tree is rendered, or rejected if the project path * fails to load. */ function _loadProject(rootPath) { if (_projectRoot) { // close current project $(exports).triggerHandler("beforeProjectClose", _projectRoot); } // close all the old files DocumentManager.closeAll(); // reset tree node id's _projectInitialLoad.id = 0; var result = new $.Deferred(), resultRenderTree; // restore project tree state from last time this project was open _projectInitialLoad.previous = _prefs.getValue(_getTreeStateKey(rootPath)) || []; // Populate file tree as long as we aren't running in the browser if (!brackets.inBrowser) { // Point at a real folder structure on local disk NativeFileSystem.requestNativeFileSystem(rootPath, function (rootEntry) { var projectRootChanged = (!_projectRoot || !rootEntry) || _projectRoot.fullPath !== rootEntry.fullPath; // Success! var perfTimerName = PerfUtils.markStart("Load Project: " + rootPath); _projectRoot = rootEntry; // The tree will invoke our "data provider" function to populate the top-level items, then // go idle until a node is expanded - at which time it'll call us again to fetch the node's // immediate children, and so on. resultRenderTree = _renderTree(_treeDataProvider); resultRenderTree.done(function () { if (projectRootChanged) { $(exports).triggerHandler("projectOpen", _projectRoot); } result.resolve(); }); resultRenderTree.fail(function () { PerfUtils.terminateMeasurement(perfTimerName); result.reject(); }); resultRenderTree.always(function () { PerfUtils.addMeasurement(perfTimerName); }); }, function (error) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_LOADING_PROJECT, StringUtils.format( Strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, StringUtils.htmlEscape(rootPath), error.code, function () { result.reject(); } ) ).done(function () { // The project folder stored in preference doesn't exist, so load the default // project directory. // TODO (issue #267): When Brackets supports having no project directory // defined this code will need to change return _loadProject(_getDefaultProjectPath()); }); } ); } return result.promise(); } /** * Open a new project. Currently, Brackets must always have a project open, so * this method handles both closing the current project and opening a new project. * * @param {string=} path Optional absolute path to the root folder of the project. * If path is undefined or null, displays a dialog where the user can choose a * folder to load. If the user cancels the dialog, nothing more happens. * @return {$.Promise} A promise object that will be resolved when the * project is loaded and tree is rendered, or rejected if the project path * fails to load. */ function openProject(path) { var result = new $.Deferred(); // Confirm any unsaved changes first. We run the command in "prompt-only" mode, meaning it won't // actually close any documents even on success; we'll do that manually after the user also oks //the folder-browse dialog. CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true }) .done(function () { if (path) { // use specified path _loadProject(path).pipe(result.resolve, result.reject); } else { // Pop up a folder browse dialog NativeFileSystem.showOpenDialog(false, true, "Choose a folder", _projectRoot.fullPath, null, function (files) { // If length == 0, user canceled the dialog; length should never be > 1 if (files.length > 0) { // Load the new project into the folder tree _loadProject(files[0]).pipe(result.resolve, result.reject); } else { result.reject(); } }, function (error) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_LOADING_PROJECT, StringUtils.format(Strings.OPEN_DIALOG_ERROR, error.code) ); result.reject(); } ); } }) .fail(function () { result.reject(); }); // if fail, don't open new project: user canceled (or we failed to save its unsaved changes) return result.promise(); } /** * Create a new item in the project tree. * * @param baseDir {string} Full path of the directory where the item should go * @param initialName {string} Initial name for the item * @param skipRename {boolean} If true, don't allow the user to rename the item * @return {$.Promise} A promise object that will be resolved with the FileEntry * of the created object, or rejected if the user cancelled or entered an illegal * filename. */ function createNewItem(baseDir, initialName, skipRename) { var node = null, selection = _projectTree.jstree("get_selected"), selectionEntry = null, position = "inside", escapeKeyPressed = false, result = new $.Deferred(), wasNodeOpen = true; // get the FileEntry or DirectoryEntry if (selection) { selectionEntry = selection.data("entry"); } // move selection to parent DirectoryEntry if (selectionEntry) { if (selectionEntry.isFile) { position = "after"; var parent = $.jstree._reference(_projectTree)._get_parent(selection); if (typeof (parent.data) === "function") { // get Entry from tree node // note that the jstree root will return undefined selectionEntry = parent.data("entry"); } else { // reset here. will be replaced with project root. selectionEntry = null; } } else if (selectionEntry.isDirectory) { wasNodeOpen = selection.hasClass("jstree-open"); } } // use the project root DirectoryEntry if (!selectionEntry) { selectionEntry = getProjectRoot(); } _projectTree.on("create.jstree", function (event, data) { $(event.target).off("create.jstree"); function errorCleanup() { // TODO (issue #115): If an error occurred, we should allow the user to fix the filename. // For now we just remove the node so you have to start again. var parent = data.inst._get_parent(data.rslt.obj); _projectTree.jstree("remove", data.rslt.obj); // Restore tree node state and styling when errors occur. // parent returns -1 when at the root if (parent && (parent !== -1)) { var methodName = (wasNodeOpen) ? "open_node" : "close_node"; var classToAdd = (wasNodeOpen) ? "jstree-open" : "jstree-closed"; // This is a workaround for issue #149 where jstree would show this node as a leaf. _projectTree.jstree(methodName, parent); parent.removeClass("jstree-leaf jstree-closed jstree-open") .addClass(classToAdd); } result.reject(); } if (!escapeKeyPressed) { // Validate file name // TODO (issue #270): There are some filenames like COM1, LPT3, etc. that are not valid on Windows. // We may want to add checks for those here. // See http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx if (data.rslt.name.search(/[\/?*:;\{\}<>\\|]+/) !== -1) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.INVALID_FILENAME_TITLE, Strings.INVALID_FILENAME_MESSAGE ); errorCleanup(); return; } // Use getFile() to create the new file selectionEntry.getFile( data.rslt.name, {create: true, exclusive: true}, function (entry) { data.rslt.obj.data("entry", entry); _projectTree.jstree("select_node", data.rslt.obj, true); result.resolve(entry); }, function (error) { if ((error.code === FileError.PATH_EXISTS_ERR) || (error.code === FileError.TYPE_MISMATCH_ERR)) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.INVALID_FILENAME_TITLE, StringUtils.format(Strings.FILE_ALREADY_EXISTS, StringUtils.htmlEscape(data.rslt.name)) ); } else { var errString = error.code === FileError.NO_MODIFICATION_ALLOWED_ERR ? Strings.NO_MODIFICATION_ALLOWED_ERR : StringUtils.format(String.GENERIC_ERROR, error.code); var errMsg = StringUtils.format(Strings.ERROR_CREATING_FILE, StringUtils.htmlEscape(data.rslt.name), errString); Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_CREATING_FILE_TITLE, errMsg ); } errorCleanup(); } ); } else { //escapeKeyPressed errorCleanup(); } }); // TODO (issue #115): Need API to get tree node for baseDir. // In the meantime, pass null for node so new item is placed // relative to the selection node = selection; // Open the node before creating the new child _projectTree.jstree("open_node", node); // Create the node and open the editor _projectTree.jstree("create", node, position, {data: initialName}, null, skipRename); if (!skipRename) { var $renameInput = _projectTree.find(".jstree-rename-input"); $renameInput.on("keydown", function (event) { // Listen for escape key on keydown, so we can remove the node in the create.jstree handler above if (event.keyCode === 27) { escapeKeyPressed = true; } }); ViewUtils.scrollElementIntoView(_projectTree, $renameInput, true); } return result.promise(); } /** * Forces createNewItem() to complete by removing focus from the rename field which causes * the new file to be written to disk */ function forceFinishRename() { $(".jstree-rename-input").blur(); } // Initialize variables and listeners that depend on the HTML DOM $(brackets).on("htmlContentLoadComplete", function () { $projectTreeContainer = $("#project-files-container"); $("#open-files-container").on("contentChanged", function () { _redraw(false); // redraw jstree when working set size changes }); }); // Init PreferenceStorage var defaults = { projectPath: _getDefaultProjectPath() /* initialize to brackets source */ }; _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaults); // Event Handlers $(FileViewController).on("documentSelectionFocusChange", _documentSelectionFocusChange); $(FileViewController).on("fileViewFocusChange", _fileViewFocusChange); // Commands CommandManager.register(Strings.CMD_OPEN_FOLDER, Commands.FILE_OPEN_FOLDER, openProject); // Define public API exports.getProjectRoot = getProjectRoot; exports.isWithinProject = isWithinProject; exports.makeProjectRelativeIfPossible = makeProjectRelativeIfPossible; exports.shouldShow = shouldShow; exports.openProject = openProject; exports.getSelectedItem = getSelectedItem; exports.getInitialProjectPath = getInitialProjectPath; exports.createNewItem = createNewItem; exports.forceFinishRename = forceFinishRename; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * DocumentManager maintains a list of currently 'open' Documents. It also owns the list of files in * the working set, and the notion of which Document is currently shown in the main editor UI area. * * Document is the model for a file's contents; it dispatches events whenever those contents change. * To transiently inspect a file's content, simply get a Document and call getText() on it. However, * to be notified of Document changes or to modify a Document, you MUST call addRef() to ensure the * Document instance 'stays alive' and is shared by all other who read/modify that file. ('Open' * Documents are all Documents that are 'kept alive', i.e. have ref count > 0). * * To get a Document, call getDocumentForPath(); never new up a Document yourself. * * Secretly, a Document may use an Editor instance to act as the model for its internal state. (This * is unavoidable because CodeMirror does not separate its model from its UI). Documents are not * modifiable until they have a backing 'master Editor'. Creation of the backing Editor is owned by * EditorManager. A Document only gets a backing Editor if it becomes the currentDocument, or if edits * occur in any Editor (inline or full-sized) bound to the Document; there is currently no other way * to ensure a Document is modifiable. * * A non-modifiable Document may still dispatch change notifications, if the Document was changed * externally on disk. * * Aside from the text content, Document tracks a few pieces of metadata - notably, whether there are * any unsaved changes. * * This module dispatches several events: * - dirtyFlagChange -- When any Document's isDirty flag changes. The 2nd arg to the listener is the * Document whose flag changed. * - documentSaved -- When a Document's changes have been saved. The 2nd arg to the listener is the * Document that has been saved. * - currentDocumentChange -- When the value of getCurrentDocument() changes. * - workingSetAdd -- When a file is added to the working set (see getWorkingSet()). The 2nd arg * to the listener is the added FileEntry. * - workingSetAddList -- When a list of files are added to the working set (e.g. project open, multiple file open). * The 2nd arg to the listener is the array of added FileEntry objects. * - workingSetRemove -- When a file is removed from the working set (see getWorkingSet()). The * 2nd arg to the listener is the removed FileEntry. * - workingSetRemoveList -- When a list of files is to be removed from the working set (e.g. project close). * The 2nd arg to the listener is the array of removed FileEntry objects. * * These are jQuery events, so to listen for them you do something like this: * $(DocumentManager).on("eventname", handler); */ define('document/DocumentManager',['require','exports','module','file/NativeFileSystem','project/ProjectManager','editor/EditorManager','preferences/PreferencesManager','file/FileUtils','command/CommandManager','utils/Async','utils/PerfUtils','command/Commands'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ProjectManager = require("project/ProjectManager"), EditorManager = require("editor/EditorManager"), PreferencesManager = require("preferences/PreferencesManager"), FileUtils = require("file/FileUtils"), CommandManager = require("command/CommandManager"), Async = require("utils/Async"), PerfUtils = require("utils/PerfUtils"), Commands = require("command/Commands"); /** * Unique PreferencesManager clientID */ var PREFERENCES_CLIENT_ID = "com.adobe.brackets.DocumentManager"; /** * @private * @see DocumentManager.getCurrentDocument() */ var _currentDocument = null; /** * @private * @type {PreferenceStorage} */ var _prefs = {}; /** * Returns the Document that is currently open in the editor UI. May be null. * When this changes, DocumentManager dispatches a "currentDocumentChange" event. The current * document always has a backing Editor (Document._masterEditor != null) and is thus modifiable. * @return {?Document} */ function getCurrentDocument() { return _currentDocument; } /** * @private * @type {Array.<FileEntry>} * @see DocumentManager.getWorkingSet() */ var _workingSet = []; /** * @private * Contains the same set of items as _workinSet, but ordered by how recently they were _currentDocument (0 = most recent). * @type {Array.<FileEntry>} */ var _workingSetMRUOrder = []; /** * While true, the MRU order is frozen * @type {boolean} */ var _documentNavPending = false; /** * While true, allow preferences to be saved * @type {boolean} */ var _isProjectChanging = false; /** * All documents with refCount > 0. Maps Document.file.fullPath -> Document. * @private * @type {Object.<string, Document>} */ var _openDocuments = {}; /** * Returns a list of items in the working set in UI list order. May be 0-length, but never null. * * When a file is added this list, DocumentManager dispatches a "workingSetAdd" event. * When a file is removed from list, DocumentManager dispatches a "workingSetRemove" event. * To listen for ALL changes to this list, you must listen for both events. * * Which items belong in the working set is managed entirely by DocumentManager. Callers cannot * (yet) change this collection on their own. * * @return {Array.<FileEntry>} */ function getWorkingSet() { return _workingSet; // TODO: (issue #297) return a clone to prevent meddling? } /** * Returns the index of the file matching fullPath in the working set. * Returns -1 if not found. * @param {!string} fullPath * @param {Array.<FileEntry>=} list Pass this arg to search a different array of files. Internal * use only. * @returns {number} index */ function findInWorkingSet(fullPath, list) { list = list || _workingSet; var ret = -1; var found = list.some(function findByPath(file, i) { ret = i; return file.fullPath === fullPath; }); return (found ? ret : -1); } /** * Returns all Documents that are 'open' in the UI somewhere (for now, this means open in an * inline editor and/or a full-size editor). Only these Documents can be modified, and only * these Documents are synced with external changes on disk. * @return {Array.<Document>} */ function getAllOpenDocuments() { var result = []; var path; for (path in _openDocuments) { if (_openDocuments.hasOwnProperty(path)) { result.push(_openDocuments[path]); } } return result; } /** * Adds the given file to the end of the working set list, if it is not already in the list. * Does not change which document is currently open in the editor. Completes synchronously. * @param {!FileEntry} file */ function addToWorkingSet(file) { // If doc is already in working set, don't add it again if (findInWorkingSet(file.fullPath) !== -1) { return; } // Add _workingSet.push(file); // Add to MRU order: either first or last, depending on whether it's already the current doc or not if (_currentDocument && _currentDocument.file.fullPath === file.fullPath) { _workingSetMRUOrder.unshift(file); } else { _workingSetMRUOrder.push(file); } // Dispatch event $(exports).triggerHandler("workingSetAdd", file); } /** * Adds the given file list to the end of the working set list. * Does not change which document is currently open in the editor. * More efficient than calling addToWorkingSet() (in a loop) for * a list of files because there's only 1 redraw at the end * @param {!FileEntryArray} fileList */ function addListToWorkingSet(fileList) { var uniqueFileList = []; // Process only files not already in working set fileList.forEach(function (file) { // If doc is already in working set, don't add it again if (findInWorkingSet(file.fullPath) === -1) { uniqueFileList.push(file); // Add _workingSet.push(file); // Add to MRU order: either first or last, depending on whether it's already the current doc or not if (_currentDocument && _currentDocument.file.fullPath === file.fullPath) { _workingSetMRUOrder.unshift(file); } else { _workingSetMRUOrder.push(file); } } }); // Dispatch event $(exports).triggerHandler("workingSetAddList", [uniqueFileList]); } /** * Removes the given file from the working set list, if it was in the list. Does not change * the current editor even if it's for this file. * @param {!FileEntry} file */ function removeFromWorkingSet(file) { // If doc isn't in working set, do nothing var index = findInWorkingSet(file.fullPath); if (index === -1) { return; } // Remove _workingSet.splice(index, 1); _workingSetMRUOrder.splice(findInWorkingSet(file.fullPath, _workingSetMRUOrder), 1); // Dispatch event $(exports).triggerHandler("workingSetRemove", file); } /** * Removes all files from the working set list. */ function _removeAllFromWorkingSet() { var fileList = _workingSet; // Remove all _workingSet = []; _workingSetMRUOrder = []; // Dispatch event $(exports).triggerHandler("workingSetRemoveList", [fileList]); } /** * Moves document to the front of the MRU list, IF it's in the working set; no-op otherwise. * @param {!Document} */ function _markMostRecent(doc) { var mruI = findInWorkingSet(doc.file.fullPath, _workingSetMRUOrder); if (mruI !== -1) { _workingSetMRUOrder.splice(mruI, 1); _workingSetMRUOrder.unshift(doc.file); } } /** * Indicate that changes to currentDocument are temporary for now, and should not update the MRU * ordering of the working set. Useful for next/previous keyboard navigation (until Ctrl is released) * or for incremental-search style document preview like Quick Open will eventually have. * Can be called any number of times, and ended by a single finalizeDocumentNavigation() call. */ function beginDocumentNavigation() { _documentNavPending = true; } /** * Un-freezes the MRU list after one or more beginDocumentNavigation() calls. Whatever document is * current is bumped to the front of the MRU list. */ function finalizeDocumentNavigation() { if (_documentNavPending) { _documentNavPending = false; _markMostRecent(_currentDocument); } } /** * Changes currentDocument to the given Document, firing currentDocumentChange, which in turn * causes this Document's main editor UI to be shown in the editor pane, updates the selection * in the file tree / working set UI, etc. This call may also add the item to the working set. * * @param {!Document} document The Document to make current. May or may not already be in the * working set. */ function setCurrentDocument(doc) { // If this doc is already current, do nothing if (_currentDocument === doc) { return; } var perfTimerName = PerfUtils.markStart("setCurrentDocument:\t" + doc.file.fullPath); // If file not within project tree, add it to working set right now (don't wait for it to // become dirty) if (!ProjectManager.isWithinProject(doc.file.fullPath)) { addToWorkingSet(doc.file); } // Adjust MRU working set ordering (except while in the middle of a Ctrl+Tab sequence) if (!_documentNavPending) { _markMostRecent(doc); } // Make it the current document _currentDocument = doc; $(exports).triggerHandler("currentDocumentChange"); // (this event triggers EditorManager to actually switch editors in the UI) PerfUtils.addMeasurement(perfTimerName); } /** Changes currentDocument to null, causing no full Editor to be shown in the UI */ function _clearCurrentDocument() { // If editor already blank, do nothing if (!_currentDocument) { return; } // Change model & dispatch event _currentDocument = null; $(exports).triggerHandler("currentDocumentChange"); // (this event triggers EditorManager to actually clear the editor UI) } /** * Closes the full editor for the given file (if there is one), and removes it from the working * set. Any other editors for this Document remain open. Discards any unsaved changes - it is * expected that the UI has already confirmed with the user before calling this. * * Changes currentDocument if this file was the current document (may change to null). * * This is a subset of notifyFileDeleted(). Use this for the user-facing Close command. * * @param {!FileEntry} file */ function closeFullEditor(file) { // If this was the current document shown in the editor UI, we're going to switch to a // different document (or none if working set has no other options) if (_currentDocument && _currentDocument.file.fullPath === file.fullPath) { var wsIndex = findInWorkingSet(file.fullPath); // Decide which doc to show in editor after this one var nextFile; if (wsIndex === -1) { // If doc wasn't in working set, use bottommost working set item if (_workingSet.length > 0) { nextFile = _workingSet[_workingSet.length - 1]; } // else: leave nextDocument null; editor area will be blank } else { // If doc was in working set, use item next to it (below if possible) if (wsIndex < _workingSet.length - 1) { nextFile = _workingSet[wsIndex + 1]; } else if (wsIndex > 0) { nextFile = _workingSet[wsIndex - 1]; } // else: leave nextDocument null; editor area will be blank } // Switch editor to next document (or blank it out) if (nextFile) { CommandManager.execute(Commands.FILE_OPEN, { fullPath: nextFile.fullPath }); } else { _clearCurrentDocument(); } } // (Now we're guaranteed that the current document is not the one we're closing) console.assert(!(_currentDocument && _currentDocument.file.fullPath === file.fullPath)); // Remove closed doc from working set, if it was in there // This happens regardless of whether the document being closed was the current one or not removeFromWorkingSet(file); // Note: EditorManager will dispose the closed document's now-unneeded editor either in // response to the editor-swap call above, or the removeFromWorkingSet() call, depending on // circumstances. See notes in EditorManager for more. } /** * Equivalent to calling closeFullEditor() for all Documents. Same caveat: this discards any * unsaved changes, so the UI should confirm with the user before calling this. */ function closeAll() { _clearCurrentDocument(); _removeAllFromWorkingSet(); } /** * Cleans up any loose Documents whose only ref is its own master Editor, and that Editor is not * rooted in the UI anywhere. This can happen if the Editor is auto-created via Document APIs that * trigger _ensureMasterEditor() without making it dirty. E.g. a command invoked on the focused * inline editor makes no-op edits or does a read-only operation. */ function _gcDocuments() { getAllOpenDocuments().forEach(function (doc) { // Is the only ref to this document its own master Editor? if (doc._refCount === 1 && doc._masterEditor) { // Destroy the Editor if it's not being kept alive by the UI EditorManager._destroyEditorIfUnneeded(doc); } }); } /** * @constructor * Model for the contents of a single file and its current modification state. * See DocumentManager documentation for important usage notes. * * Document dispatches these events: * * change -- When the text of the editor changes (including due to undo/redo). * * Passes ({Document}, {ChangeList}), where ChangeList is a linked list (NOT an array) * of change record objects. Each change record looks like: * * { from: start of change, expressed as {line: <line number>, ch: <character offset>}, * to: end of change, expressed as {line: <line number>, ch: <chracter offset>}, * text: array of lines of text to replace existing text, * next: next change record in the linked list, or undefined if this is the last record } * * The line and ch offsets are both 0-based. * * The ch offset in "from" is inclusive, but the ch offset in "to" is exclusive. For example, * an insertion of new content (without replacing existing content) is expressed by a range * where from and to are the same. * * If "from" and "to" are undefined, then this is a replacement of the entire text content. * * IMPORTANT: If you listen for the "change" event, you MUST also addRef() the document * (and releaseRef() it whenever you stop listening). You should also listen to the "deleted" * event. * * (FUTURE: this is a modified version of the raw CodeMirror change event format; may want to make * it an ordinary array) * * deleted -- When the file for this document has been deleted. All views onto the document should * be closed. The document will no longer be editable or dispatch "change" events. * * @param {!FileEntry} file Need not lie within the project. * @param {!Date} initialTimestamp File's timestamp when we read it off disk. * @param {!string} rawText Text content of the file. */ function Document(file, initialTimestamp, rawText) { if (!(this instanceof Document)) { // error if constructor called without 'new' throw new Error("Document constructor must be called with 'new'"); } if (_openDocuments[file.fullPath]) { throw new Error("Creating a document when one already exists, for: " + file); } this.file = file; this.refreshText(rawText, initialTimestamp); // This is a good point to clean up any old dangling Documents _gcDocuments(); } /** * Number of clients who want this Document to stay alive. The Document is listed in * DocumentManager._openDocuments whenever refCount > 0. */ Document.prototype._refCount = 0; /** * The FileEntry for this document. Need not lie within the project. * @type {!FileEntry} */ Document.prototype.file = null; /** * Whether this document has unsaved changes or not. * When this changes on any Document, DocumentManager dispatches a "dirtyFlagChange" event. * @type {boolean} */ Document.prototype.isDirty = false; /** * What we expect the file's timestamp to be on disk. If the timestamp differs from this, then * it means the file was modified by an app other than Brackets. * @type {!Date} */ Document.prototype.diskTimestamp = null; /** * The text contents of the file, or null if our backing model is _masterEditor. * @type {?string} */ Document.prototype._text = null; /** * Editor object representing the full-size editor UI for this document. May be null if Document * has not yet been modified or been the currentDocument; in that case, our backing model is the * string _text. * @type {?Editor} */ Document.prototype._masterEditor = null; /** * The content's line-endings style. If a Document is created on empty text, or text with * inconsistent line endings, defaults to the current platform's standard endings. * @type {FileUtils.LINE_ENDINGS_CRLF|FileUtils.LINE_ENDINGS_LF} */ Document.prototype._lineEndings = null; /** Add a ref to keep this Document alive */ Document.prototype.addRef = function () { //console.log("+++REF+++ "+this); if (this._refCount === 0) { //console.log("+++ adding to open list"); if (_openDocuments[this.file.fullPath]) { throw new Error("Document for this path already in _openDocuments!"); } _openDocuments[this.file.fullPath] = this; $(exports).triggerHandler("afterDocumentCreate", this); } this._refCount++; }; /** Remove a ref that was keeping this Document alive */ Document.prototype.releaseRef = function () { //console.log("---REF--- "+this); this._refCount--; if (this._refCount < 0) { throw new Error("Document ref count has fallen below zero!"); } if (this._refCount === 0) { //console.log("--- removing from open list"); if (!_openDocuments[this.file.fullPath]) { throw new Error("Document with references was not in _openDocuments!"); } $(exports).triggerHandler("beforeDocumentDelete", this); delete _openDocuments[this.file.fullPath]; } }; /** * Attach a backing Editor to the Document, enabling setText() to be called. Assumes Editor has * already been initialized with the value of getText(). ONLY Editor should call this (and only * when EditorManager has told it to act as the master editor). * @param {!Editor} masterEditor */ Document.prototype._makeEditable = function (masterEditor) { if (this._masterEditor) { throw new Error("Document is already editable"); } else { this._text = null; this._masterEditor = masterEditor; $(masterEditor).on("change", this._handleEditorChange.bind(this)); } }; /** * Detach the backing Editor from the Document, disallowing setText(). The text content is * stored back onto _text so other Document clients continue to have read-only access. ONLY * Editor.destroy() should call this. */ Document.prototype._makeNonEditable = function () { if (!this._masterEditor) { throw new Error("Document is already non-editable"); } else { // _text represents the raw text, so fetch without normalized line endings this._text = this.getText(true); this._masterEditor = null; } }; /** * Guarantees that _masterEditor is non-null. If needed, asks EditorManager to create a new master * editor bound to this Document (which in turn causes Document._makeEditable() to be called). * Should ONLY be called by Editor and Document. */ Document.prototype._ensureMasterEditor = function () { if (!this._masterEditor) { EditorManager._createFullEditorForDocument(this); } }; /** * Returns the document's current contents; may not be saved to disk yet. Whenever this * value changes, the Document dispatches a "change" event. * * @param {boolean=} useOriginalLineEndings If true, line endings in the result depend on the * Document's line endings setting (based on OS & the original text loaded from disk). * If false, line endings are always \n (like all the other Document text getter methods). * @return {string} */ Document.prototype.getText = function (useOriginalLineEndings) { if (this._masterEditor) { // CodeMirror.getValue() always returns text with LF line endings; fix up to match line // endings preferred by the document, if necessary var codeMirrorText = this._masterEditor._codeMirror.getValue(); if (useOriginalLineEndings) { if (this._lineEndings === FileUtils.LINE_ENDINGS_CRLF) { return codeMirrorText.replace(/\n/g, "\r\n"); } } return codeMirrorText; } else { // Optimized path that doesn't require creating master editor if (useOriginalLineEndings) { return this._text; } else { return this._text.replace(/\r\n/g, "\n"); } } }; /** * Sets the contents of the document. Treated as an edit. Line endings will be rewritten to * match the document's current line-ending style. * @param {!string} text The text to replace the contents of the document with. */ Document.prototype.setText = function (text) { this._ensureMasterEditor(); this._masterEditor._codeMirror.setValue(text); // _handleEditorChange() triggers "change" event }; /** * Sets the contents of the document. Treated as reloading the document from disk: the document * will be marked clean with a new timestamp, the undo/redo history is cleared, and we re-check * the text's line-ending style. CAN be called even if there is no backing editor. * @param {!string} text The text to replace the contents of the document with. * @param {!Date} newTimestamp Timestamp of file at the time we read its new contents from disk. */ Document.prototype.refreshText = function (text, newTimestamp) { var perfTimerName = PerfUtils.markStart("refreshText:\t" + (!this.file || this.file.fullPath)); if (this._masterEditor) { this._masterEditor._resetText(text); // _handleEditorChange() triggers "change" event for us } else { this._text = text; // We fake a change record here that looks like CodeMirror's text change records, but // omits "from" and "to", by which we mean the entire text has changed. // TODO: Dumb to split it here just to join it again in the change handler, but this is // the CodeMirror change format. Should we document our change format to allow this to // either be an array of lines or a single string? $(this).triggerHandler("change", [this, {text: text.split(/\r?\n/)}]); } this._markClean(); this.diskTimestamp = newTimestamp; // Sniff line-ending style this._lineEndings = FileUtils.sniffLineEndings(text); if (!this._lineEndings) { this._lineEndings = FileUtils.getPlatformLineEndings(); } PerfUtils.addMeasurement(perfTimerName); }; /** * Adds, replaces, or removes text. If a range is given, the text at that range is replaced with the * given new text; if text == "", then the entire range is effectively deleted. If 'end' is omitted, * then the new text is inserted at that point and all existing text is preserved. Line endings will * be rewritten to match the document's current line-ending style. * @param {!string} text Text to insert or replace the range with * @param {!{line:number, ch:number}} start Start of range, inclusive (if 'to' specified) or insertion point (if not) * @param {?{line:number, ch:number}} end End of range, exclusive; optional */ Document.prototype.replaceRange = function (text, start, end) { this._ensureMasterEditor(); this._masterEditor._codeMirror.replaceRange(text, start, end); // _handleEditorChange() triggers "change" event }; /** * Returns the characters in the given range. Line endings are normalized to '\n'. * @param {!{line:number, ch:number}} start Start of range, inclusive * @param {!{line:number, ch:number}} end End of range, exclusive * @return {!string} */ Document.prototype.getRange = function (start, end) { this._ensureMasterEditor(); return this._masterEditor._codeMirror.getRange(start, end); }; /** * Returns the text of the given line (excluding any line ending characters) * @param {number} Zero-based line number * @return {!string} */ Document.prototype.getLine = function (lineNum) { this._ensureMasterEditor(); return this._masterEditor._codeMirror.getLine(lineNum); }; /** * Batches a series of related Document changes. Repeated calls to replaceRange() should be wrapped in a * batch for efficiency. Begins the batch, calls doOperation(), ends the batch, and then returns. * @param {function()} doOperation */ Document.prototype.batchOperation = function (doOperation) { this._ensureMasterEditor(); this._masterEditor._codeMirror.operation(doOperation); }; /** * Handles changes from the master backing Editor. Changes are triggered either by direct edits * to that Editor's UI, OR by our setText()/refreshText() methods. * @private */ Document.prototype._handleEditorChange = function (event, editor, changeList) { // On any change, mark the file dirty. In the future, we should make it so that if you // undo back to the last saved state, we mark the file clean. var wasDirty = this.isDirty; this.isDirty = editor._codeMirror.isDirty(); // If file just became dirty, notify listeners, and add it to working set (if not already there) if (wasDirty !== this.isDirty) { $(exports).triggerHandler("dirtyFlagChange", [this]); addToWorkingSet(this.file); } // Notify that Document's text has changed // TODO: This needs to be kept in sync with SpecRunnerUtils.createMockDocument(). In the // future, we should fix things so that we either don't need mock documents or that this // is factored so it will just run in both. $(this).triggerHandler("change", [this, changeList]); }; /** * @private */ Document.prototype._markClean = function () { this.isDirty = false; if (this._masterEditor) { this._masterEditor._codeMirror.markClean(); } $(exports).triggerHandler("dirtyFlagChange", this); }; /** * Called when the document is saved (which currently happens in DocumentCommandHandlers). Marks the * document not dirty and notifies listeners of the save. */ Document.prototype.notifySaved = function () { if (!this._masterEditor) { console.log("### Warning: saving a Document that is not modifiable!"); } this._markClean(); $(exports).triggerHandler("documentSaved", this); // TODO: (issue #295) fetching timestamp async creates race conditions (albeit unlikely ones) var thisDoc = this; this.file.getMetadata( function (metadata) { thisDoc.diskTimestamp = metadata.modificationTime; }, function (error) { console.log("Error updating timestamp after saving file: " + thisDoc.file.fullPath); } ); }; /* (pretty toString(), to aid debugging) */ Document.prototype.toString = function () { var dirtyInfo = (this.isDirty ? " (dirty!)" : " (clean)"); var editorInfo = (this._masterEditor ? " (Editable)" : " (Non-editable)"); var refInfo = " refs:" + this._refCount; return "[Document " + this.file.fullPath + dirtyInfo + editorInfo + refInfo + "]"; }; /** * Gets an existing open Document for the given file, or creates a new one if the Document is * not currently open ('open' means referenced by the UI somewhere). Always use this method to * get Documents; do not call the Document constructor directly. This method is safe to call * in parallel. * * If you are going to hang onto the Document for more than just the duration of a command - e.g. * if you are going to display its contents in a piece of UI - then you must addRef() the Document * and listen for changes on it. (Note: opening the Document in an Editor automatically manages * refs and listeners for that Editor UI). * * @param {!string} fullPath * @return {$.Promise} A promise object that will be resolved with the Document, or rejected * with a FileError if the file is not yet open and can't be read from disk. */ function getDocumentForPath(fullPath) { var doc = _openDocuments[fullPath], pendingPromise = getDocumentForPath._pendingDocumentPromises[fullPath]; if (doc) { // use existing document return new $.Deferred().resolve(doc).promise(); } else if (pendingPromise) { // wait for the result of a previous request return pendingPromise; } else { var result = new $.Deferred(), promise = result.promise(); // log this document's Promise as pending getDocumentForPath._pendingDocumentPromises[fullPath] = promise; // create a new document var fileEntry = new NativeFileSystem.FileEntry(fullPath), perfTimerName = PerfUtils.markStart("getDocumentForPath:\t" + fullPath); result.done(function () { PerfUtils.addMeasurement(perfTimerName); }).fail(function () { PerfUtils.finalizeMeasurement(perfTimerName); }); FileUtils.readAsText(fileEntry) .always(function () { // document is no longer pending delete getDocumentForPath._pendingDocumentPromises[fullPath]; }) .done(function (rawText, readTimestamp) { doc = new Document(fileEntry, readTimestamp, rawText); result.resolve(doc); }) .fail(function (fileError) { result.reject(fileError); }); return promise; } } /** * Document promises that are waiting to be resolved. It is possible for multiple clients * to request the same document simultaneously before the initial request has completed. * In particular, this happens at app startup where the working set is created and the * intial active document is opened in an editor. This is essential to ensure that only * 1 Document exists for any FileEntry. * @private * @type {Object.<string, $.Promise>} */ getDocumentForPath._pendingDocumentPromises = {}; /** * Returns the existing open Document for the given file, or null if the file is not open ('open' * means referenced by the UI somewhere). If you will hang onto the Document, you must addRef() * it; see {@link getDocumentForPath()} for details. */ function getOpenDocumentForPath(fullPath) { return _openDocuments[fullPath]; } /** * Reacts to a file being deleted: if there is a Document for this file, causes it to dispatch a * "deleted" event; ensures it's not the currentDocument; and removes this file from the working * set. These actions in turn cause all open editors for this file to close. Discards any unsaved * changes - it is expected that the UI has already confirmed with the user before calling. * * To simply close a main editor when the file hasn't been deleted, use closeFullEditor() or FILE_CLOSE. * * FUTURE: Instead of an explicit notify, we should eventually listen for deletion events on some * sort of "project file model," making this just a private event handler. */ function notifyFileDeleted(file) { // First ensure it's not currentDocument, and remove from working set closeFullEditor(file); // Notify all other editors to close as well var doc = getOpenDocumentForPath(file.fullPath); if (doc) { $(doc).triggerHandler("deleted"); } // At this point, all those other views SHOULD have released the Doc if (doc && doc._refCount > 0) { console.log("WARNING: deleted Document still has " + doc._refCount + " references. Did someone addRef() without listening for 'deleted'?"); } } /** * Get the next or previous file in the working set, in MRU order (relative to currentDocument). * @param {Number} inc -1 for previous, +1 for next; no other values allowed * @return {?FileEntry} null if working set empty */ function getNextPrevFile(inc) { if (inc !== -1 && inc !== +1) { throw new Error("Illegal argument: inc = " + inc); } if (_currentDocument) { var mruI = findInWorkingSet(_currentDocument.file.fullPath, _workingSetMRUOrder); if (mruI === -1) { // If doc not in working set, return most recent working set item if (_workingSetMRUOrder.length > 0) { return _workingSetMRUOrder[0]; } } else { // If doc is in working set, return next/prev item with wrap-around var newI = mruI + inc; if (newI >= _workingSetMRUOrder.length) { newI = 0; } else if (newI < 0) { newI = _workingSetMRUOrder.length - 1; } return _workingSetMRUOrder[newI]; } } // If no doc open or working set empty, there is no "next" file return null; } /** * @private * Preferences callback. Saves the document file paths for the working set. */ function _savePreferences() { if (_isProjectChanging) { return; } // save the working set file paths var files = [], isActive = false, workingSet = getWorkingSet(), currentDoc = getCurrentDocument(), projectRoot = ProjectManager.getProjectRoot(); if (!projectRoot) { return; } workingSet.forEach(function (file, index) { // flag the currently active editor isActive = currentDoc && (file.fullPath === currentDoc.file.fullPath); files.push({ file: file.fullPath, active: isActive }); }); // append file root to make file list unique for each project _prefs.setValue("files_" + projectRoot.fullPath, files); } /** * @private * Handle beforeProjectClose event */ function _beforeProjectClose() { _savePreferences(); // When app is shutdown via shortcut key, the command goes directly to the // app shell, so we can't reliably fire the beforeProjectClose event on // app shutdown. To compensate, we listen for currentDocumentChange, // workingSetAdd, and workingSetRemove events so that the prefs for // last project used get updated. But when switching projects, after // the beforeProjectChange event gets fired, DocumentManager.closeAll() // causes workingSetRemove event to get fired and update the prefs to an empty // list. So, temporarily (until projectOpen event) disallow saving prefs. _isProjectChanging = true; } /** * @private * Initializes the working set. */ function _projectOpen() { _isProjectChanging = false; // file root is appended for each project var projectRoot = ProjectManager.getProjectRoot(), files = _prefs.getValue("files_" + projectRoot.fullPath); if (!files) { return; } var filesToOpen = [], activeFile; // Add all files to the working set without verifying that // they still exist on disk (for faster project switching) files.forEach(function (value, index) { filesToOpen.push(new NativeFileSystem.FileEntry(value.file)); if (value.active) { activeFile = value.file; } }); addListToWorkingSet(filesToOpen); // Initialize the active editor if (!activeFile && _workingSet.length > 0) { activeFile = _workingSet[0].fullPath; } if (activeFile) { CommandManager.execute(Commands.FILE_OPEN, { fullPath: activeFile }); } } // Define public API exports.Document = Document; exports.getCurrentDocument = getCurrentDocument; exports.getDocumentForPath = getDocumentForPath; exports.getOpenDocumentForPath = getOpenDocumentForPath; exports.getWorkingSet = getWorkingSet; exports.findInWorkingSet = findInWorkingSet; exports.getAllOpenDocuments = getAllOpenDocuments; exports.setCurrentDocument = setCurrentDocument; exports.addToWorkingSet = addToWorkingSet; exports.addListToWorkingSet = addListToWorkingSet; exports.removeFromWorkingSet = removeFromWorkingSet; exports.getNextPrevFile = getNextPrevFile; exports.beginDocumentNavigation = beginDocumentNavigation; exports.finalizeDocumentNavigation = finalizeDocumentNavigation; exports.closeFullEditor = closeFullEditor; exports.closeAll = closeAll; exports.notifyFileDeleted = notifyFileDeleted; // Setup preferences _prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID); $(exports).bind("currentDocumentChange workingSetAdd workingSetAddList workingSetRemove workingSetRemoveList", _savePreferences); // Performance measurements PerfUtils.createPerfMeasurement("DOCUMENT_MANAGER_GET_DOCUMENT_FOR_PATH", "DocumentManager.getDocumentForPath()"); // Handle project change events $(ProjectManager).on("projectOpen", _projectOpen); $(ProjectManager).on("beforeProjectClose", _beforeProjectClose); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50*/ /*global $, define, brackets, FileError */ define('utils/NativeApp',['require','exports','module','utils/Async'],function (require, exports, module) { var Async = require("utils/Async"); /** * @private * Map an fs error code to a FileError. */ function _browserErrToFileError(err) { if (err === brackets.fs.ERR_NOT_FOUND) { return FileError.NOT_FOUND_ERR; } // All other errors are mapped to the generic "security" error return FileError.SECURITY_ERR; } var liveBrowserOpenedPIDs = []; var liveBrowserUserDataDir = ""; /** openLiveBrowser * * @param {string} url * @return {$.Promise} */ function openLiveBrowser(url, enableRemoteDebugging) { var result = new $.Deferred(); brackets.app.openLiveBrowser(url, enableRemoteDebugging, function onRun(err, pid) { if (!err) { liveBrowserOpenedPIDs.push(pid); result.resolve(pid); } else { result.reject(_browserErrToFileError(err)); } }, liveBrowserUserDataDir); return result.promise(); } /** closeLiveBrowser * * @return {$.Promise} */ function closeLiveBrowser(pid) { var result = new $.Deferred(); if (isNaN(pid)) { pid = 0; } console.log("calling to close: " + pid); brackets.app.closeLiveBrowser(function (err) { console.log("called closing: " + pid + " with err: " + err); if (!err) { var i = liveBrowserOpenedPIDs.indexOf(pid); if (i !== -1) { liveBrowserOpenedPIDs.splice(i, 1); } result.resolve(); } else { result.reject(_browserErrToFileError(err)); } }, pid); return result.promise(); } /** closeAllLiveBrowsers * Closes all the browsers that were tracked on open * TODO: does not seem to work on Windows * @return {$.Promise} */ function closeAllLiveBrowsers() { //make a copy incase the array is edited as we iterate var closeIDs = liveBrowserOpenedPIDs.concat(); return Async.doSequentially(closeIDs, closeLiveBrowser, false); } /** _setLiveBrowserUserDataDir * For Unit Tests only, changes the default dir the browser use for it's user data * @return {$.Promise} */ function _setLiveBrowserUserDataDir(path) { liveBrowserUserDataDir = path; } /** * Opens a URL in the system default browser */ function openURLInDefaultBrowser(url) { brackets.app.openURLInDefaultBrowser(function (err) {}, url); } // Define public API exports.openLiveBrowser = openLiveBrowser; exports.closeLiveBrowser = closeLiveBrowser; exports.closeAllLiveBrowsers = closeAllLiveBrowsers; exports.openURLInDefaultBrowser = openURLInDefaultBrowser; //API for Unit Tests exports._setLiveBrowserUserDataDir = _setLiveBrowserUserDataDir; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, WebSocket, FileError, window, XMLHttpRequest */ /** * Inspector manages the connection to Chrome/Chromium's remote debugger. * See inspector.html for the documentation of the remote debugger. * * # SETUP * * To enable remote debugging in Chrome or Chromium open either application * with the following parameters: * * --enable-remote-debugger --remote-debugging-port=9222 * * This will open an HTTP server on the specified port, which can be used to * browse the available remote debugger sessions. In general, every open * browser tab can host an individual remote debugger session. The * available interfaces can be exported by requesting: * * http://127.0.0.1:9222/json * * The response is a JSON-formatted array that specifies all available remote * debugger sessions including the remote debugging web sockets. * * Inspector can connect directly to a web socket via `connect(socketURL)`, or * it can find the web socket that corresponds to the tab at the given URL and * connect to it via `connectToURL(url)`. The later returns a promise. To * disconnect use `disconnect()`. * * # EVENTS * * Inspector dispatches several connectivity-related events + all remote debugger * events (see below). Event handlers are attached via `on(event, function)` and * detached via `off(event, function)`. * * `connect` Inspector did successfully connect to the remote debugger * `disconnect` Inspector did disconnect from the remote debugger * `error` Inspector encountered an error * `message` Inspector received a message from the remote debugger - this * provides a low-level entry point to remote debugger events * * # REMOTE DEBUGGER COMMANDS * * Commands are executed by calling `{Domain}.{Command}()` with the parameters * specified in the order of the remote debugger documentation. These command * functions are generated automatically at runtime from Inspector.json. The * actual implementation of these functions is found in * `_send(method, signature, varargs)`, which verifies, serializes, and * transmits the command to the remote debugger. If the last parameter of any * command function call is a function, it will be used as the callback. * * # REMOTE DEBUGGER EVENTS * * Debugger events are dispatched as regular events using {Domain}.{Event} as * the event name. The handler function will be called with a single parameter * that stores all returned values as an object. */ define('LiveDevelopment/Inspector/Inspector',['require','exports','module'],function Inspector(require, exports, module) { // jQuery exports object for events var $exports = $(exports); var _messageId = 1; // id used for remote method calls, auto-incrementing var _messageCallbacks = {}; // {id -> function} for remote method calls var _socket; // remote debugger WebSocket var _connectDeferred; // The deferred connect /** Check a parameter value against the given signature * This only checks for optional parameters, not types * Type checking is complex because of $ref and done on the remote end anyways * @param {signature} * @param {value} */ function _verifySignature(signature, value) { if (value === undefined) { console.assert(signature.optional === true, "Missing argument: " + signature.name); } return true; } /** Send a message to the remote debugger * All passed arguments after the signature are passed on as parameters. * If the last argument is a function, it is used as the callback function. * @param {string} remote method * @param {object} the method signature */ function _send(method, signature, varargs) { if (!_socket) { // FUTURE: Our current implementation closes and re-opens an inspector connection whenever // a new HTML file is selected. If done quickly enough, pending requests from the previous // connection could come in before the new socket connection is established. For now we // simply ignore this condition. // This race condition will go away once we support multiple inspector connections and turn // off auto re-opening when a new HTML file is selected. return; } console.assert(_socket, "You must connect to the WebSocket before sending messages."); var id, callback, args, i, params = {}; // extract the parameters, the callback function, and the message id args = Array.prototype.slice.call(arguments, 2); if (typeof args[args.length - 1] === "function") { id = _messageId++; _messageCallbacks[id] = args.pop(); } else { id = 0; } // verify the parameters against the method signature // this also constructs the params object of type {name -> value} for (i in signature) { if (_verifySignature(args[i], signature[i])) { params[signature[i].name] = args[i]; } } _socket.send(JSON.stringify({ method: method, id: id, params: params })); } /** WebSocket did close */ function _onDisconnect() { _socket = undefined; $exports.triggerHandler("disconnect"); } /** WebSocket reported an error */ function _onError(error) { $exports.triggerHandler("error", [error]); } /** WebSocket did open */ function _onConnect() { $exports.triggerHandler("connect"); } /** Received message from the WebSocket * A message can be one of three things: * 1. an error -> report it * 2. the response to a previous command -> run the stored callback * 3. an event -> trigger an event handler method * @param {object} message */ function _onMessage(message) { var response = JSON.parse(message.data); $exports.triggerHandler("message", [response]); if (response.error) { $exports.triggerHandler("error", [response.error]); } else if (response.result) { if (_messageCallbacks[response.id]) { _messageCallbacks[response.id](response.result); } } else { var domainAndMethod = response.method.split("."); var domain = domainAndMethod[0]; var method = domainAndMethod[1]; $(exports[domain]).triggerHandler(method, response.params); } } /** Public Functions *****************************************************/ /** Get the available debugger sockets from the remote debugger * @param {string} host IP or name * @param {integer} debugger port */ function getAvailableSockets(host, port) { if (!host) { host = "127.0.0.1"; } if (!port) { port = 9222; } var def = new $.Deferred(); var request = new XMLHttpRequest(); request.open("GET", "http://" + host + ":" + port + "/json"); request.onload = function onLoad() { var sockets = JSON.parse(request.response); def.resolve(sockets); }; request.onerror = function onError() { def.reject(request.response); }; request.send(null); return def.promise(); } /** Register a handler to be called when the given event is triggered * @param {string} event name * @param {function} handler function */ function on(name, handler) { $exports.on(name, handler); } /** Remove the given or all event handler(s) for the given event or remove all event handlers * @param {string} optional event name * @param {function} optional handler function */ function off(name, handler) { $exports.off(name, handler); } /** Disconnect from the remote debugger WebSocket */ function disconnect() { if (_socket) { if (_socket.readyState === 1) { _socket.close(); } else { delete _socket.onmessage; delete _socket.onopen; delete _socket.onclose; delete _socket.onerror; } _socket = undefined; } } /** Connect to the remote debugger WebSocket at the given URL * @param {string} WebSocket URL */ function connect(socketURL) { disconnect(); _socket = new WebSocket(socketURL); _socket.onmessage = _onMessage; _socket.onopen = _onConnect; _socket.onclose = _onDisconnect; _socket.onerror = _onError; } /** Connect to the remote debugger of the page that is at the given URL * @param {string} url */ function connectToURL(url) { if (_connectDeferred) { // reject an existing connection attempt _connectDeferred.reject("CANCEL"); } var deferred = new $.Deferred(); _connectDeferred = deferred; var promise = getAvailableSockets(); promise.done(function onGetAvailableSockets(response) { if (deferred.isRejected()) { return; } var i, page; for (i in response) { page = response[i]; if (page.webSocketDebuggerUrl && page.url.search(url) === 0) { connect(page.webSocketDebuggerUrl); deferred.resolve(); return; } } deferred.reject(FileError.ERR_NOT_FOUND); // Reject with a "not found" error }); promise.fail(function onFail(err) { deferred.reject(err); }); return deferred.promise(); } /** Check if the inspector is connected */ function connected() { return _socket !== undefined; } /** Initialize the Inspector * Read the Inspector.json configuration and define the command objects * -> Inspector.domain.command() */ function init(theConfig) { exports.config = theConfig; var request = new XMLHttpRequest(); request.open("GET", "LiveDevelopment/Inspector/Inspector.json"); request.onload = function onLoad() { var InspectorJSON = JSON.parse(request.response); var i, j, domain, domainDef, command; for (i in InspectorJSON.domains) { domain = InspectorJSON.domains[i]; exports[domain.domain] = {}; for (j in domain.commands) { command = domain.commands[j]; exports[domain.domain][command.name] = _send.bind(undefined, domain.domain + "." + command.name, command.parameters); } } }; request.send(null); } // Export public functions exports.getAvailableSockets = getAvailableSockets; exports.on = on; exports.off = off; exports.disconnect = disconnect; exports.connect = connect; exports.connectToURL = connectToURL; exports.connected = connected; exports.init = init; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, XMLHttpRequest */ /** * RemoteAgent defines and provides an interface for custom remote functions * loaded from RemoteFunctions. Remote commands are executed via * `call(name, varargs)`. * * Remote events are dispatched as events on this object. */ define('LiveDevelopment/Agents/RemoteAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector'],function RemoteAgent(require, exports, module) { var $exports = $(exports); var Inspector = require("LiveDevelopment/Inspector/Inspector"); var _load; // deferred load var _objectId; // the object id of the remote object // WebInspector Event: Page.loadEventFired function _onLoadEventFired(event, res) { // res = {timestamp} var request = new XMLHttpRequest(); request.open("GET", "LiveDevelopment/Agents/RemoteFunctions.js"); request.onload = function onLoad() { var run = "window._LD=" + request.response + "()"; Inspector.Runtime.evaluate(run, function onEvaluate(res) { console.assert(!res.wasThrown, res.result.description); _objectId = res.result.objectId; _load.resolve(); }); }; request.send(null); } // WebInspector Event: DOM.attributeModified function _onAttributeModified(event, res) { // res = {nodeId, name, value} var matches = /^data-ld-(.*)/.exec(res.name); if (matches) { $exports.triggerHandler(matches[1], res); } } /** Call a remote function * The parameters are passed on to the remote functions. Nodes are resolved * and sent as objectIds. * @param {string} function name */ function call(method, varargs) { console.assert(_objectId, "Attempted to call remote method without objectId set."); var args = Array.prototype.slice.call(arguments, 1); // if the last argument is a function it is the callback function var callback; if (typeof args[args.length - 1] === "function") { callback = args.pop(); } // Resolve node parameters var i; for (i in args) { if (args[i].nodeId) { args[i] = args[i].resolve(); } } $.when.apply(undefined, args).then(function onResolvedAllNodes() { var i, arg, params = []; for (i in arguments) { arg = args[i]; if (arg.objectId) { params.push({objectId: arg.objectId}); } else { params.push({value: arg}); } } Inspector.Runtime.callFunctionOn(_objectId, "_LD." + method, params, undefined, callback); }); } /** Initialize the agent */ function load() { _load = new $.Deferred(); $(Inspector.Page).on("loadEventFired.RemoteAgent", _onLoadEventFired); $(Inspector.DOM).on("attributeModified.RemoteAgent", _onAttributeModified); return _load.promise(); } /** Clean up */ function unload() { $(Inspector.Page).off(".RemoteAgent"); $(Inspector.DOM).off(".RemoteAgent"); } // Export public functions exports.call = call; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * DOMHelpers is a collection of functions used by the DOMAgent exports `eachNode(src, callback)` */ define('LiveDevelopment/Agents/DOMHelpers',['require','exports','module'],function DOMHelpersModule(require, exports, module) { /** Test if the given character is a quote character * {char} source character * {char} escape (previous) character * {char} quote character */ function _isQuote(c, escape, quote) { if (escape === "\\") { return false; } if (quote !== undefined) { return c === quote; } return c === "\"" || c === "'"; } /** Remove quotes from the string and adjust escaped quotes * @param {string} source string */ function _removeQuotes(src) { if (_isQuote(src[0]) && src[src.length - 1] === src[0]) { var q = src[0]; src = src.substr(1, src.length - 2); src = src.replace("\\" + q, q); } return src; } /** Find the next match using several constraints * @param {string} source string * @param {string} or [{regex}, {length}] the match definition * @param {integer} ignore characters before this offset * @param {boolean} watch for quotes * @param [{string},{string}] watch for comments */ function _find(src, match, skip, quotes, comments) { if (typeof match === "string") { match = [match, match.length]; } if (skip === undefined) { skip = 0; } var i, activeQuote, isComment = false; for (i = skip; i < src.length; i++) { if (quotes && _isQuote(src[i], src[i - 1], activeQuote)) { // starting quote activeQuote = activeQuote ? undefined : src[i]; } else if (!activeQuote) { if (comments && !isComment && src.substr(i, comments[0].length) === comments[0]) { // opening comment isComment = true; i += comments[0].length - 1; } else if (isComment) { // we are commented if (src.substr(i, comments[1].length) === comments[1]) { isComment = false; i += comments[1].length - 1; } } else if (src.substr(i, match[1]).search(match[0]) === 0) { // match return i; } } } return -1; } /** Callback iterator using `_find` */ function _findEach(src, match, quotes, comments, callback) { var from = 0; var to; while (from < src.length) { to = _find(src, match, from, quotes, comments); if (to < 0) { to = src.length; } callback(src.substr(from, to - from)); from = to + 1; } } /** Find the next tag * @param {string} source string * @param {integer} ignore characters before this offset */ function _findTag(src, skip) { var from, to, inc; from = _find(src, [/<[a-z!\/]/, 2], skip); if (from < 0) { return null; } if (src.substr(from, 4) === "<!--") { // html comments to = _find(src, "-->", from + 4); inc = 3; } else if (src.substr(from, 7) === "<script") { // script tag to = _find(src, "</script>", from + 7); inc = 9; } else { to = _find(src, ">", from + 1, true); inc = 1; } if (to < 0) { return null; } return {from: from, length: to + inc - from}; } /** Extract tag attributes from the given source of a single tag * @param {string} source content */ function _extractAttributes(content) { // remove the node name and the closing bracket and optional slash content = content.replace(/^<\S+\s*/, ""); content = content.replace(/\s*\/?>$/, ""); if (content.length === 0) { return; } // go through the items and identify key value pairs split by = var index, key, value; var attributes = {}; _findEach(content, [/\s/, 1], true, undefined, function each(item) { index = item.search("="); if (index < 0) { return; } // get the key key = item.substr(0, index).trim(); if (key.length === 0) { return; } // get the value value = item.substr(index + 1).trim(); value = _removeQuotes(value); attributes[key] = value; }); return attributes; } /** Extract the node payload * @param {string} source content */ function extractPayload(content) { var payload = {}; if (content[0] !== "<") { // text payload.nodeType = 3; payload.nodeValue = content; } else if (content.substr(0, 4) === "<!--") { // comment payload.nodeType = 8; payload.nodeValue = content.substr(4, content.length - 7); } else if (content[1] === "!") { // doctype payload.nodeType = 10; } else { // regular element payload.nodeType = 1; payload.nodeName = /^<([^>\s]+)/.exec(content)[1].toUpperCase(); payload.attributes = _extractAttributes(content); // closing node (/ at the beginning) if (payload.nodeName[0] === "/") { payload.nodeName = payload.nodeName.substr(1); payload.closing = true; } // closed node (/ at the end) if (content[content.length - 2] === "/") { payload.closed = true; } } return payload; } /** Split the source string into payloads representing individual nodes * @param {string} source * @param {function(payload)} callback */ // split a string into individual node contents function eachNode(src, callback) { var index = 0; var text, range, length, payload; var x = 0; while (index < src.length) { // find the next tag range = _findTag(src, index); if (!range) { range = { from: src.length, length: 0 }; } // add the text before the tag length = range.from - index; if (length > 0) { text = src.substr(index, length); if (/\S/.test(text)) { payload = extractPayload(text); payload.sourceOffset = index; payload.sourceLength = length; callback(payload); } } // add the tag if (range.length > 0) { payload = extractPayload(src.substr(range.from, range.length)); payload.sourceOffset = range.from; payload.sourceLength = range.length; callback(payload); } // advance index = range.from + range.length; } } // Export public functions exports.extractPayload = extractPayload; exports.eachNode = eachNode; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * DOMNode represents a node in the DOM tree. It is constructed from a payload * similar to {DOM.Node} and supports all basic tree operations. If a node has * a nodeId it is registered with the `DOMAgent` via `addNode()`. The node's * sourceOffset and sourceLength is stored as its location and length. Nodes can * iterated using `each()` or `find()`. `dump` shows the entire tree on the console. */ define('LiveDevelopment/Agents/DOMNode',['require','exports','module','LiveDevelopment/Agents/DOMHelpers'],function DOMNodeModule(require, exports, module) { var DOMHelpers = require("LiveDevelopment/Agents/DOMHelpers"); /** Fill a string to the given length (used for debug output) * @param {string} source string * @param {integer} length * @param {char} fill character */ function _fill(string, length, c) { if (c === undefined) { c = " "; } while (string.length < length) { string += c; } return string; } /** Construct a find condition (used in `find` and `findParent`) * The match can be a callback returning true or false, the node * name or the node type. * @param {function} or {string} or {number} match criteria */ function _makeFindCondition(match) { switch (typeof match) { case "function": return match; case "string": return function findCondition(name, node) { return node.name === name; }.bind(undefined, match.toUpperCase()); case "number": return function findCondition(type, node) { return node.type === type; }.bind(undefined, match); default: console.error("Invalid find condition: " + match); } } /** Constructor * @param {DOMAgent} the agent is passed to avoid circular relationships * @param {DOM.Node} node payload */ var DOMNode = function DOMNode(agent, payload) { this.agent = agent; this.children = []; this.attributes = {}; // set the payload if (typeof payload === "string") { payload = DOMHelpers.extractPayload(payload); } if (payload) { this.setPayload(payload); } this.agent.addNode(this); }; var TYPE_ELEMENT = DOMNode.TYPE_ELEMENT = 1; // element node var TYPE_ATTRIBUTE = DOMNode.TYPE_ATTRIBUTE = 2; // attribute node (unused) var TYPE_TEXT = DOMNode.TYPE_TEXT = 3; // text node var TYPE_COMMENT = DOMNode.TYPE_COMMENT = 8; // comment node <!-- --> var TYPE_DOCUMENT = DOMNode.TYPE_DOCUMENT = 9; // document node <!DOCUMENT> /** Remove a node */ DOMNode.prototype.remove = function remove() { this.agent.removeNode(this); if (this.parent) { this.parent.removeChild(this); } }; /** Node Payload ***********************************************************/ /** Set the node payload * @param {DOM.Node} payload */ DOMNode.prototype.setPayload = function setPayload(payload) { this.nodeId = payload.nodeId; this.type = payload.nodeType; if (payload.nodeName) { this.name = payload.nodeName; } if (payload.nodeValue) { this.value = payload.nodeValue; } this.attributes = {}; if (payload.attributes) { var i, k, v; for (i = 0; i < payload.attributes.length; i += 2) { k = payload.attributes[i]; v = payload.attributes[i + 1]; this.attributes[k] = v; } } if (payload.sourceOffset) { this.location = payload.sourceOffset; } if (payload.sourceLength) { this.length = payload.sourceLength; } else { if (this.value) { this.length = this.value.length; } else if (this.name) { this.length = this.name.length + 2; } } if (payload.children) { this.setChildrenPayload(payload.children); } else if (payload.childNodeCount) { this.agent.requestChildNodes(this); } }; /** Create child nodes from the given payload * @param [{DOM.Node}] payload of the children */ DOMNode.prototype.setChildrenPayload = function setChildrenPayload(childrenPayload) { var i, payload, node; for (i in childrenPayload) { payload = childrenPayload[i]; node = new DOMNode(this.agent, payload); this.appendChild(node); } }; /** Construct the payload for this node */ DOMNode.prototype.payload = function payload() { var res = { type: this.type }; if (this.nodeType === TYPE_ELEMENT) { res.nodeName = this.name; } else { res.value = this.value; } return res; }; /** Find the next node that matches the given payload * @param {DOM.Node} payload */ DOMNode.prototype.findParentForNextNodeMatchingPayload = function findParentForNextNodeMatchingPayload(payload) { var parent = this.canHaveChildren() ? this : this.parent; while (parent && !parent.matchesPayload(payload)) { parent = parent.parent; } return parent; }; /** Find the next node that matches the given payload * @param {DOM.Node} payload */ DOMNode.prototype.findNextNodeMatchingPayload = function findNextNodeMatchingPayload(payload) { var next = this.nextNode(); while (next && !next.matchesPayload(payload)) { next = next.nextNode(); } return next; }; /** Test if the node matches the given payload * @param {DOM.Node} payload */ DOMNode.prototype.matchesPayload = function matchesPayload(payload) { var r = false; if (this.type === payload.nodeType) { switch (this.type) { case 1: r = this.name === payload.nodeName; break; case 3: // TODO payload.nodeValue's HTML Entities must be decoded // r = this.value === payload.nodeValue; r = true; break; default: r = true; } } // Useful output for debugging this - do not remove // console.debug(this.type + "," + this.name + "," + this.value + " = " + payload.nodeType + "," + payload.nodeName + "," + payload.value + " -> " + r); return r; }; /** Resolve the node and retrieve its objectId from the remote debugger */ DOMNode.prototype.resolve = function resolve() { var def = new $.Deferred(); if (this.objectId) { def.resolve(this); } else if (!this.nodeId) { def.reject(); } else { this.agent.resolveNode(this, function onResolve(res) { this.objectId = res.object.objectId; def.resolve(this); }.bind(this)); } return def.promise(); }; /** Tree Operations ******************************************************/ /** Can the node have children? */ DOMNode.prototype.canHaveChildren = function canHaveChildren() { return (this.type === 1 && !this.closed && !this.closing && this.nodeName !== "LINK"); }; /** Remove a child * @param {DOMNode} child node to remove */ DOMNode.prototype.removeChild = function removeChild(node) { this.children.splice(this.indexOfChild(node), 1); delete node.parent; }; /** Insert a child node at the given index * @param {DOMNode} node to insert * @param {integer} optional index (node is appended if missing) */ DOMNode.prototype.insertChildAt = function insertChildAt(node, index) { if (node.parent) { node.parent.removeChild(node); } if (!index || index < 0 || index > this.children.length) { index = this.children.length; } this.children.splice(index, 0, node); node.parent = this; return node; }; /** Append a child to this node * @param {DOMNode} child node to append */ DOMNode.prototype.appendChild = function appendChild(node) { return this.insertChildAt(node); }; /** Insert a child node after the given node * @param {DOMNode} child node to insert * @param {DOMNode} existing child node */ DOMNode.prototype.insertChildAfter = function insertChildAfter(node, sibling) { var index = this.indexOfChild(sibling); if (index >= 0) { index++; } return this.insertChildAt(node, index); }; /** Insert a child node before the given node * @param {DOMNode} child node to insert * @param {DOMNode} existing child node */ DOMNode.prototype.insertChildBefore = function insertChildBefore(node, sibling) { var index = this.indexOfChild(sibling); return this.insertChildAt(node, index); }; /** Determine the index of a child node * @param {DOMNode} child node */ DOMNode.prototype.indexOfChild = function indexOfChild(node) { if (!node) { return -1; } var i; for (i in this.children) { if (this.children[i] === node) { return parseInt(i, 0); } } return -1; }; /** Get the previous sibling */ DOMNode.prototype.previousSibling = function previousSibling() { if (!this.parent) { return null; } return this.parent.children[this.parent.indexOfChild(this) - 1]; }; /** Get the next sibling */ DOMNode.prototype.nextSibling = function nextSibling() { if (!this.parent) { return null; } return this.parent.children[this.parent.indexOfChild(this) + 1]; }; /** Get the previous node */ DOMNode.prototype.previousNode = function previousNode() { var node = this.previousSibling(); if (node) { if (node.children.length > 0) { node = node.children[node.children.length - 1]; } } else { node = this.parent; } return node; }; /** Get the next node */ DOMNode.prototype.nextNode = function nextNode() { if (this.children.length > 0) { // return the first child return this.children[0]; } // return this or any ancestor's next sibling var node, parent = this; while (parent) { node = parent.nextSibling(); if (node) { return node; } parent = parent.parent; } return null; }; /** Traverse the tree * @param {function({DOM.Node})} called for this node and all descendants */ DOMNode.prototype.each = function each(callback) { if (callback(this) === false) { return false; } var i; for (i in this.children) { if (this.children[i].each(callback) === false) { return false; } } return true; }; /** Find a node in the tree * @param {function} or {string} or {integer} find condition */ DOMNode.prototype.find = function find(match) { var findCondition = _makeFindCondition(match); var node = null; this.each(function each(n) { if (findCondition(n)) { node = n; return false; } }); return node; }; /** Find all nodes with the given find condition * @param {function} or {string} or {integer} find condition */ DOMNode.prototype.findAll = function findAll(match) { var nodes = []; var findCondition = _makeFindCondition(match); this.each(function each(node) { if (findCondition(node)) { nodes.push(node); } }); return nodes; }; /** Iterate over all parent nodes * @param {function({DOM.Node})} called for each ancestor */ DOMNode.prototype.eachParent = function eachParent(callback) { var node = this.parent; while (node) { if (callback(node) === false) { return; } node = node.parent; } return null; }; /** Find a parent node that matches the find condition * @param {function} or {string} or {integer} find condition */ DOMNode.prototype.findParent = function findParent(findCondition) { var theParent = null; this.eachParent(function each(parent) { if (findCondition(parent)) { theParent = parent; return false; } }); return theParent; }; /** Find the root of the tree */ DOMNode.prototype.root = function root() { var node = this; while (node.parent) { node = node.parent; } return node; }; /** Node Info ***********************************************************/ /** Test if the given location is inside this node * @param {integer} location * @param {boolean} also include children */ DOMNode.prototype.isAtLocation = function isAtLocation(location, includeChildren) { if (includeChildren === undefined) { includeChildren = true; } if (!this.location || location < this.location) { return false; } var to; if (includeChildren && this.closeLocation) { to = this.closeLocation + this.closeLength; } else { to = this.location + this.length; } if (this.type === TYPE_TEXT) { to += 1; } return location < to; }; /** Test if this node is empty */ DOMNode.prototype.isEmpty = function isEmpty() { return this.type === TYPE_TEXT && /^\s*$/.test(this.value); }; /** Debug Output */ DOMNode.prototype.toString = function toString() { var r; switch (this.type) { case TYPE_ELEMENT: r = "<" + this.name + ">"; break; case TYPE_ATTRIBUTE: r = "[ATTRIBUTE]"; break; case TYPE_TEXT: r = this.value.replace(/\s+/, " ").substr(0, 40); break; case TYPE_COMMENT: r = "<!--" + this.value.replace(/\s+/, " ").substr(0, 33) + "-->"; break; case TYPE_DOCUMENT: r = "<!DOCTYPE>"; break; } return r; }; /** Detailed Debug Output */ DOMNode.prototype.dump = function dump(pre) { if (pre === undefined) { pre = ""; } var r = pre + this.toString(); if (this.location) { r = _fill(r, 60); r += " (" + this.location + "," + (this.location + this.length) + ")"; if (this.closeLocation) { r += " (" + this.closeLocation + "," + (this.closeLocation + this.closeLength) + ")"; } } if (this.nodeId) { r = _fill(r, 80); r += " {" + this.nodeId + "}"; } console.info(r); pre += ". "; var i; for (i in this.children) { this.children[i].dump(pre); } }; return DOMNode; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, XMLHttpRequest */ /** * DOMAgent constructs and maintains a tree of {DOMNode}s that represents the * rendered DOM tree in the remote browser. Nodes can be accessed by id or * location (source offset). To update the DOM tree in response to a change of * the source document (replace [from,to] with text) call * `applyChange(from, to, text)`. * * The DOMAgent triggers `getDocument` once it has loaded * the document. */ define('LiveDevelopment/Agents/DOMAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/RemoteAgent','LiveDevelopment/Agents/DOMNode','LiveDevelopment/Agents/DOMHelpers'],function DOMAgent(require, exports, module) { var $exports = $(exports); var Inspector = require("LiveDevelopment/Inspector/Inspector"); var RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); var DOMNode = require("LiveDevelopment/Agents/DOMNode"); var DOMHelpers = require("LiveDevelopment/Agents/DOMHelpers"); var _load; // {$.Deferred} load promise var _idToNode; // {nodeId -> node} var _pendingRequests; // {integer} number of pending requests before initial loading is complete /** Get the last node before the given location * @param {integer} location */ function nodeBeforeLocation(location) { var node; exports.root.each(function each(n) { if (!n.location || location < n.location) { return true; } if (!node || node.location < n.location) { node = n; } }); return node; } /** Get the element node that encloses the given location * @param {location} */ function allNodesAtLocation(location) { var nodes = []; exports.root.each(function each(n) { if (n.type === DOMNode.TYPE_ELEMENT && n.isAtLocation(location)) { nodes.push(n); } }); return nodes; } /** Get the node at the given location * @param {location} */ function nodeAtLocation(location) { return exports.root.find(function each(n) { return n.isAtLocation(location, false); }); } /** Find the node for the given id * @param {DOMNode} node */ function nodeWithId(nodeId) { return _idToNode[nodeId]; } /** Update the node index * @param {DOMNode} node */ function removeNode(node) { if (node.nodeId) { delete _idToNode[node.nodeId]; } } /** Update the node index * @param {DOMNode} node */ function addNode(node) { if (node.nodeId) { _idToNode[node.nodeId] = node; } } /** Request the child nodes for a node * @param {DOMNode} node */ function requestChildNodes(node) { if (_pendingRequests >= 0) { _pendingRequests++; } Inspector.DOM.requestChildNodes(node.nodeId); } /** Resolve a node * @param {DOMNode} node */ function resolveNode(node, callback) { console.assert(node.nodeId, "Attempted to resolve node without id"); Inspector.DOM.resolveNode(node.nodeId, callback); } /** Eliminate the query string from a URL * @param {string} URL */ function _cleanURL(url) { var index = url.search(/[#\?]/); if (index >= 0) { url = url.substr(0, index); } return url; } /** Map the DOM document to the source text * @param {string} source */ function _mapDocumentToSource(source) { var node = exports.root; DOMHelpers.eachNode(source, function each(payload) { if (!node) { return true; } if (payload.closing) { var parent = node.findParentForNextNodeMatchingPayload(payload); if (!parent) { return console.warn("Matching Parent not at " + payload.sourceOffset + " (" + payload.nodeName + ")"); } parent.closeLocation = payload.sourceOffset; parent.closeLength = payload.sourceLength; } else { var next = node.findNextNodeMatchingPayload(payload); if (!next) { return console.warn("Skipping Source Node at " + payload.sourceOffset); } node = next; node.location = payload.sourceOffset; node.length = payload.sourceLength; if (payload.closed) { node.closed = payload.closed; } } }); } /** Load the source document and match it with the DOM tree*/ function _onFinishedLoadingDOM() { console.assert(exports.url.substr(0, 7) === "file://", "Can only load file urls"); var request = new XMLHttpRequest(); request.open("GET", exports.url); request.onload = function onLoad() { _mapDocumentToSource(request.response); _load.resolve(); }; request.onerror = function onError() { _load.reject("Could not load source file at " + exports.url); }; request.send(null); } // WebInspector Event: Page.loadEventFired function _onLoadEventFired(event, res) { // res = {timestamp} Inspector.DOM.getDocument(function onGetDocument(res) { $exports.triggerHandler("getDocument", res); // res = {root} _idToNode = {}; _pendingRequests = 0; exports.root = new DOMNode(exports, res.root); }); } // WebInspector Event: Page.frameNavigated function _onFrameNavigated(event, res) { // res = {frame} exports.url = _cleanURL(res.frame.url); } // WebInspector Event: DOM.documentUpdated function _onDocumentUpdated(event, res) { // res = {} } // WebInspector Event: DOM.setChildNodes function _onSetChildNodes(event, res) { // res = {parentId, nodes} var node = nodeWithId(res.parentId); node.setChildrenPayload(res.nodes); if (_pendingRequests > 0 && --_pendingRequests === 0) { _onFinishedLoadingDOM(); } } // WebInspector Event: DOM.childNodeCountUpdated function _onChildNodeCountUpdated(event, res) { // res = {nodeId, childNodeCount} if (res.nodeId > 0) { Inspector.DOM.requestChildNodes(res.nodeId); } } // WebInspector Event: DOM.childNodeInserted function _onChildNodeInserted(event, res) { // res = {parentNodeId, previousNodeId, node} if (res.node.nodeId > 0) { var parent = nodeWithId(res.parentNodeId); var previousNode = nodeWithId(res.previousNodeId); var node = new DOMNode(exports, res.node); parent.insertChildAfter(node, previousNode); } } // WebInspector Event: DOM.childNodeRemoved function _onChildNodeRemoved(event, res) { // res = {parentNodeId, nodeId} if (res.nodeId > 0) { var node = nodeWithId(res.nodeId); node.remove(); } } /** Apply a change * @param {integer} start offset of the change * @param {integer} end offset of the change * @param {string} change text */ function applyChange(from, to, text) { var delta = from - to + text.length; var node = nodeAtLocation(from); // insert a text node if (!node) { if (!(/^\s*$/).test(text)) { console.warn("Inserting nodes not supported."); node = nodeBeforeLocation(from); } } else if (node.type === 3) { // update a text node var value = node.value.substr(0, from - node.location); value += text; value += node.value.substr(to - node.location); node.value = value; Inspector.DOM.setNodeValue(node.nodeId, node.value); } else { console.warn("Changing non-text nodes not supported."); } // adjust the location of all nodes after the change if (node) { node.length += delta; exports.root.each(function each(n) { if (n.location > node.location) { n.location += delta; } }); } } /** Initialize the agent */ function load() { _load = new $.Deferred(); $(Inspector.Page) .on("frameNavigated.DOMAgent", _onFrameNavigated) .on("loadEventFired.DOMAgent", _onLoadEventFired); $(Inspector.DOM) .on("documentUpdated.DOMAgent", _onDocumentUpdated) .on("setChildNodes.DOMAgent", _onSetChildNodes) .on("childNodeCountUpdated.DOMAgent", _onChildNodeCountUpdated) .on("childNodeInserted.DOMAgent", _onChildNodeInserted) .on("childNodeRemoved.DOMAgent", _onChildNodeRemoved); Inspector.Page.enable(); Inspector.Page.reload(); return _load.promise(); } /** Clean up */ function unload() { $(Inspector.Page).off(".DOMAgent"); $(Inspector.DOM).off(".DOMAgent"); } // Export private functions exports.nodeBeforeLocation = nodeBeforeLocation; exports.allNodesAtLocation = allNodesAtLocation; exports.nodeAtLocation = nodeAtLocation; exports.nodeWithId = nodeWithId; exports.removeNode = removeNode; exports.addNode = addNode; exports.requestChildNodes = requestChildNodes; exports.applyChange = applyChange; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * HighlightAgent dispatches events for highlight requests from in-browser * highlight requests, and allows highlighting nodes and rules in the browser. * * Trigger "highlight" when a node should be highlighted */ define('LiveDevelopment/Agents/HighlightAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/RemoteAgent','LiveDevelopment/Agents/DOMAgent'],function HighlightAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); var _highlight; // active highlight // Remote Event: Highlight function _onRemoteHighlight(event, res) { var node; if (res.value === "1") { node = DOMAgent.nodeWithId(res.nodeId); } $(exports).triggerHandler("highlight", node); } /** Hide in-browser highlighting */ function hide() { switch (_highlight.type) { case "node": Inspector.DOM.hideHighlight(); break; case "css": RemoteAgent.call("hideHighlight"); break; } _highlight = {}; } /** Highlight a single node using DOM.highlightNode * @param {DOMNode} node */ function node(n) { if (!Inspector.config.highlight) { return; } // go to the parent of a text node if (n && n.type === 3) { n = n.parent; } // node cannot be highlighted if (!n || !n.nodeId || n.type !== 1) { return hide(); } // node is already highlighted if (_highlight.type === "node" && _highlight.ref === n.nodeId) { return; } // highlight the node _highlight = {type: "node", ref: n.nodeId}; Inspector.DOM.highlightNode(n.nodeId, Inspector.config.highlightConfig); } /** Highlight all nodes affected by a CSS rule * @param {string} rule selector */ function rule(name) { if (_highlight.rule === name) { return; } hide(); _highlight = {type: "css", ref: name}; RemoteAgent.call("highlightRule", name); } /** Initialize the agent */ function load() { _highlight = {}; $(RemoteAgent).on("highlight.HighlightAgent", _onRemoteHighlight); } /** Clean up */ function unload() { $(RemoteAgent).off(".HighlightAgent"); } // Export public functions exports.hide = hide; exports.node = node; exports.rule = rule; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * HTMLDocument manages a single HTML source document * * # EDITING * * Editing the document will cause the corresponding node to be updated * by calling `applyChanges` on the DOMAgent. This will only work for * altering text nodes and will break when attempting to change DOM elements * or inserting or deleting nodes. * * # HIGHLIGHTING * * HTMLDocument supports highlighting nodes from the HighlightAgent and * highlighting the DOMNode corresponding to the cursor position in the * editor. */ define('LiveDevelopment/Documents/HTMLDocument',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/DOMAgent','LiveDevelopment/Agents/HighlightAgent'],function HTMLDocumentModule(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); var HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"); /** Constructor * * @param Document the source document from Brackets */ var HTMLDocument = function HTMLDocument(doc, editor) { this.doc = doc; this.editor = editor; this.onHighlight = this.onHighlight.bind(this); this.onChange = this.onChange.bind(this); this.onCursorActivity = this.onCursorActivity.bind(this); $(HighlightAgent).on("highlight", this.onHighlight); $(this.editor).on("change", this.onChange); $(this.editor).on("cursorActivity", this.onCursorActivity); this.onCursorActivity(); }; /** Close the document */ HTMLDocument.prototype.close = function close() { $(HighlightAgent).off("highlight", this.onHighlight); $(this.editor).off("change", this.onChange); $(this.editor).off("cursorActivity", this.onCursorActivity); this.onHighlight(); }; /** Event Handlers *******************************************************/ /** Triggered on cursor activity by the editor */ HTMLDocument.prototype.onCursorActivity = function onCursorActivity(event, editor) { var codeMirror = this.editor._codeMirror; if (Inspector.config.highlight) { var location = codeMirror.indexFromPos(codeMirror.getCursor()); var node = DOMAgent.allNodesAtLocation(location).pop(); HighlightAgent.node(node); } }; /** Triggered on change by the editor */ HTMLDocument.prototype.onChange = function onChange(event, editor, change) { var codeMirror = this.editor._codeMirror; while (change) { var from = codeMirror.indexFromPos(change.from); var to = codeMirror.indexFromPos(change.to); var text = change.text.join("\n"); DOMAgent.applyChange(from, to, text); change = change.next; } }; /** Triggered by the HighlightAgent to highlight a node in the editor */ HTMLDocument.prototype.onHighlight = function onHighlight(node) { if (!node || !node.location) { if (this._highlight) { this._highlight.clear(); delete this._highlight; } return; } var codeMirror = this.editor._codeMirror; var to, from = codeMirror.posFromIndex(node.location); if (node.closeLocation) { to = node.closeLocation + node.closeLength; } else { to = node.location + node.length; } to = codeMirror.posFromIndex(to); if (this._highlight) { this._highlight.clear(); } this._highlight = codeMirror.markText(from, to, "highlight"); }; // Export the class module.exports = HTMLDocument; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, PathUtils */ /** * CSSAgent keeps track of loaded style sheets and allows reloading them * from a {Document}. */ define('LiveDevelopment/Agents/CSSAgent',['require','exports','module','thirdparty/path-utils/path-utils.min','LiveDevelopment/Inspector/Inspector'],function CSSAgent(require, exports, module) { require("thirdparty/path-utils/path-utils.min"); var Inspector = require("LiveDevelopment/Inspector/Inspector"); var _load; // {$.Deferred} load promise var _urlToStyle; // {url -> loaded} style definition /** * Create a canonicalized version of the given URL, stripping off query strings and hashes. * @param {string} url the URL to canonicalize * @return the canonicalized URL */ function _canonicalize(url) { return PathUtils.parseUrl(url).hrefNoSearch; } // WebInspector Event: Page.loadEventFired function _onLoadEventFired(event, res) { // res = {timestamp} _urlToStyle = {}; Inspector.CSS.getAllStyleSheets(function onGetAllStyleSheets(res) { var i, header; for (i in res.headers) { header = res.headers[i]; _urlToStyle[_canonicalize(header.sourceURL)] = header; } _load.resolve(); }); } /** Get a style sheet for a url * @param {string} url */ function styleForURL(url) { return _urlToStyle[_canonicalize(url)]; } /** Get a list of all loaded stylesheet files by URL */ function getStylesheetURLs() { var urls = [], url; for (url in _urlToStyle) { if (_urlToStyle.hasOwnProperty(url)) { urls.push(url); } } return urls; } /** Reload a CSS style sheet from a document * @param {Document} document */ function reloadCSSForDocument(doc) { var style = styleForURL(doc.url); console.assert(style, "Style Sheet for document not loaded: " + doc.url); Inspector.CSS.setStyleSheetText(style.styleSheetId, doc.getText()); } /** Empties a CSS style sheet given a document that has been deleted * @param {Document} document */ function clearCSSForDocument(doc) { var style = styleForURL(doc.url); console.assert(style, "Style Sheet for document not loaded: " + doc.url); Inspector.CSS.setStyleSheetText(style.styleSheetId, ""); } /** Initialize the agent */ function load() { _load = new $.Deferred(); $(Inspector.Page).on("loadEventFired.CSSAgent", _onLoadEventFired); return _load.promise(); } /** Clean up */ function unload() { $(Inspector.Page).off(".CSSAgent"); } // Export public functions exports.styleForURL = styleForURL; exports.getStylesheetURLs = getStylesheetURLs; exports.reloadCSSForDocument = reloadCSSForDocument; exports.clearCSSForDocument = clearCSSForDocument; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * CSSDocument manages a single CSS source document * * # EDITING * * Editing the document will cause the style sheet to be reloaded via the * CSSAgent, which immediately updates the appearance of the rendered document. * * # HIGHLIGHTING * * CSSDocument supports highlighting nodes from the HighlightAgent and * highlighting all DOMNode corresponding to the rule at the cursor position * in the editor. * * # EVENTS * * CSSDocument dispatches these events: * deleted - When the file for the underlying Document has been deleted. The * 2nd argument to the listener will be this CSSDocument. */ define('LiveDevelopment/Documents/CSSDocument',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/CSSAgent','LiveDevelopment/Agents/HighlightAgent'],function CSSDocumentModule(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var CSSAgent = require("LiveDevelopment/Agents/CSSAgent"); var HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"); /** Constructor * * @param Document the source document from Brackets */ var CSSDocument = function CSSDocument(doc, editor, inspector) { this.doc = doc; // FUTURE: Highlighting is currently disabled, since this code doesn't yet know // how to deal with different editors pointing at the same document. /* this.editor = editor; this._highlight = []; this.onHighlight = this.onHighlight.bind(this); this.onCursorActivity = this.onCursorActivity.bind(this); $(HighlightAgent).on("highlight", this.onHighlight); */ // Add a ref to the doc since we're listening for change events this.doc.addRef(); this.onChange = this.onChange.bind(this); this.onDeleted = this.onDeleted.bind(this); $(this.doc).on("change", this.onChange); $(this.doc).on("deleted", this.onDeleted); /* $(this.editor).on("cursorActivity", this.onCursorActivity); this.onCursorActivity(); */ // get the style sheet this.styleSheet = CSSAgent.styleForURL(this.doc.url); // WebInspector Command: CSS.getStyleSheet Inspector.CSS.getStyleSheet(this.styleSheet.styleSheetId, function callback(res) { // res = {styleSheet} this.rules = res.styleSheet.rules; }.bind(this)); // If the CSS document is dirty, push the changes into the browser now if (doc.isDirty) { CSSAgent.reloadCSSForDocument(this.doc); } }; /** Get the browser version of the StyleSheet object */ CSSDocument.prototype.getStyleSheetFromBrowser = function getStyleSheetFromBrowser() { var deferred = new $.Deferred(); // WebInspector Command: CSS.getStyleSheet Inspector.CSS.getStyleSheet(this.styleSheet.styleSheetId, function callback(res) { // res = {styleSheet} if (res.styleSheet) { deferred.resolve(res.styleSheet); } else { deferred.reject(); } }); return deferred.promise(); }; /** Get the browser version of the source */ CSSDocument.prototype.getSourceFromBrowser = function getSourceFromBrowser() { var deferred = new $.Deferred(); this.getStyleSheetFromBrowser().done(function onDone(styleSheet) { deferred.resolve(styleSheet.text); }).fail(function onFail() { deferred.reject(); }); return deferred.promise(); }; /** Close the document */ CSSDocument.prototype.close = function close() { $(this.doc).off("change", this.onChange); $(this.doc).off("deleted", this.onDeleted); this.doc.releaseRef(); /* $(HighlightAgent).off("highlight", this.onHighlight); $(this.editor).off("cursorActivity", this.onCursorActivity); this.onHighlight(); */ }; // find a rule in the given rules CSSDocument.prototype.ruleAtLocation = function ruleAtLocation(location) { var i, rule; for (i in this.rules) { rule = this.rules[i]; if (rule.selectorRange.start <= location && location <= rule.style.range.end) { return rule; } } return null; }; /** Event Handlers *******************************************************/ /** Triggered on cursor activity of the editor */ CSSDocument.prototype.onCursorActivity = function onCursorActivity(event, editor) { if (Inspector.config.highlight) { var codeMirror = this.editor._codeMirror; var location = codeMirror.indexFromPos(codeMirror.getCursor()); var rule = this.ruleAtLocation(location); if (rule) { HighlightAgent.rule(rule.selectorText); } else { HighlightAgent.hide(); } } }; /** Triggered whenever the Document is edited */ CSSDocument.prototype.onChange = function onChange(event, editor, change) { // brute force: update the CSS CSSAgent.reloadCSSForDocument(this.doc); }; /** Triggered if the Document's file is deleted */ CSSDocument.prototype.onDeleted = function onDeleted(event, editor, change) { // clear the CSS CSSAgent.clearCSSForDocument(this.doc); // shut down, since our Document is now dead this.close(); $(this).triggerHandler("deleted", [this]); }; /** Triggered by the HighlightAgent to highlight a node in the editor */ CSSDocument.prototype.onHighlight = function onHighlight(node) { // clear an existing highlight var i; for (i in this._highlight) { this._highlight[i].clear(); } this._highlight = []; if (!node || !node.location) { return; } // WebInspector Command: CSS.getMatchedStylesForNode Inspector.CSS.getMatchedStylesForNode(node.nodeId, function onGetMatchesStyles(res) { // res = {matchedCSSRules, pseudoElements, inherited} var codeMirror = this.editor._codeMirror; var i, rule, from, to; for (i in res.matchedCSSRules) { rule = res.matchedCSSRules[i]; if (rule.ruleId && rule.ruleId.styleSheetId === this.styleSheet.styleSheetId) { from = codeMirror.posFromIndex(rule.selectorRange.start); to = codeMirror.posFromIndex(rule.style.range.end); this._highlight.push(codeMirror.markText(from, to, "highlight")); } } }.bind(this)); }; // Export the class module.exports = CSSDocument; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * ScriptAgent tracks all executed scripts, defines internal breakpoints, and * interfaces with the remote debugger. */ define('LiveDevelopment/Agents/ScriptAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/DOMAgent'],function ScriptAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); var _load; // the load promise var _urlToScript; // url -> script info var _idToScript; // id -> script info var _insertTrace; // the last recorded trace of a DOM insertion /** Add a call stack trace to a node * @param {integer} node id * @param [{Debugger.CallFrame}] call stack */ function _addTraceToNode(nodeId, trace) { var node = DOMAgent.nodeWithId(nodeId); node.trace = trace; } // TODO: should the parameter to this be an ID rather than a URL? /** Get the script information for a given url * @param {string} url */ function scriptWithId(url) { return _idToScript[url]; } // TODO: Strip off query/hash strings from URL (see CSSAgent._canonicalize()) /** Get the script information for a given url * @param {string} url */ function scriptForURL(url) { return _urlToScript[url]; } // DOMAgent Event: Document root loaded function _onGetDocument(event, res) { Inspector.DOMDebugger.setDOMBreakpoint(res.root.nodeId, "subtree-modified"); _load.resolve(); } // WebInspector Event: DOM.childNodeInserted function _onChildNodeInserted(event, res) { // res = {parentNodeId, previousNodeId, node} if (_insertTrace) { var node = DOMAgent.nodeWithId(res.node.nodeId); node.trace = _insertTrace; _insertTrace = undefined; } } // TODO: Strip off query/hash strings from URL (see CSSAgent._canonicalize()) // WebInspector Event: Debugger.scriptParsed function _onScriptParsed(event, res) { // res = {scriptId, url, startLine, startColumn, endLine, endColumn, isContentScript, sourceMapURL} _idToScript[res.scriptId] = res; _urlToScript[res.url] = res; } // WebInspector Event: Debugger.scriptFailedToParse function _onScriptFailedToParse(event, res) { // res = {url, scriptSource, startLine, errorLine, errorMessage} } // WebInspector Event: Debugger.paused function _onPaused(event, res) { // res = {callFrames, reason, data} switch (res.reason) { // Exception case "exception": Inspector.Debugger.resume(); // var callFrame = res.callFrames[0]; // var script = scriptWithId(callFrame.location.scriptId); break; // DOMBreakpoint case "DOM": Inspector.Debugger.resume(); if (res.data.type === "subtree-modified" && res.data.insertion === true) { _insertTrace = res.callFrames; } break; } } /** Initialize the agent */ function load() { _urlToScript = {}; _idToScript = {}; _load = new $.Deferred(); Inspector.Debugger.enable(); Inspector.Debugger.setPauseOnExceptions("uncaught"); $(DOMAgent).on("getDocument.ScriptAgent", _onGetDocument); $(Inspector.Debugger) .on("scriptParsed.ScriptAgent", _onScriptParsed) .on("scriptFailedToParse.ScriptAgent", _onScriptFailedToParse) .on("paused.ScriptAgent", _onPaused); $(Inspector.DOM).on("childNodeInserted.ScriptAgent", _onChildNodeInserted); return _load; } /** Clean up */ function unload() { $(DOMAgent).off(".ScriptAgent"); $(Inspector.Debugger).off(".ScriptAgent"); $(Inspector.DOM).off(".ScriptAgent"); } // Export public functions exports.scriptWithId = scriptWithId; exports.scriptForURL = scriptForURL; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * JSDocument manages a single JavaScript source document * * # EDITING * * Editing the document will cause the script to be reloaded via the * ScriptAgent, which updates the implementation of all functions without * loosing any state. To support redrawing canvases, jQuery must be loaded * and a rerender method must be attached to every canvas that clears and * renders the canvas. * * # HIGHLIGHTING * * JSDocument supports highlighting nodes from the HighlightAgent. Support * for highlighting the nodes that were created / touched by the current * line is missing. */ define('LiveDevelopment/Documents/JSDocument',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/ScriptAgent','LiveDevelopment/Agents/HighlightAgent'],function JSDocumentModule(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var ScriptAgent = require("LiveDevelopment/Agents/ScriptAgent"); var HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"); /** Constructor * * @param {Document} the source document */ var JSDocument = function JSDocument(doc, editor) { this.doc = doc; this.editor = editor; this.script = ScriptAgent.scriptForURL(this.doc.url); this.onHighlight = this.onHighlight.bind(this); this.onChange = this.onChange.bind(this); this.onCursorActivity = this.onCursorActivity.bind(this); $(HighlightAgent).on("highlight", this.onHighlight); $(this.editor).on("change", this.onChange); $(this.editor).on("cursorActivity", this.onCursorActivity); this.onCursorActivity(); }; /** Close the document */ JSDocument.prototype.close = function close() { $(HighlightAgent).off("highlight", this.onHighlight); $(this.editor).off("change", this.onChange); $(this.editor).off("cursorActivity", this.onCursorActivity); this.onHighlight(); }; /** Event Handlers *******************************************************/ /** Triggered on cursor activity by the editor */ JSDocument.prototype.onCursorActivity = function onCursorActivity(event, editor) { }; /** Triggered on change by the editor */ JSDocument.prototype.onChange = function onChange(event, editor, change) { var src = this.doc.getText(); Inspector.Debugger.setScriptSource(this.script.scriptId, src, function onSetScriptSource(res) { Inspector.Runtime.evaluate("if($)$(\"canvas\").each(function(i,e){if(e.rerender)e.rerender()})"); }.bind(this)); }; /** Triggered by the HighlightAgent to highlight a node in the editor */ JSDocument.prototype.onHighlight = function onHighlight(node) { // clear an existing highlight var codeMirror = this.editor._codeMirror; var i; for (i in this._highlight) { codeMirror.setLineClass(this._highlight[i]); } this._highlight = []; if (!node || !node.trace) { return; } // go through the trace and find highlight the lines of this script var callFrame, line; for (i in node.trace) { callFrame = node.trace[i]; if (callFrame.location && callFrame.location.scriptId === this.script.scriptId) { line = callFrame.location.lineNumber; codeMirror.setLineClass(line, "highlight"); this._highlight.push(line); } } }; // Export the class module.exports = JSDocument; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * ConsoleAgent forwards all console message from the remote console to the * local console. */ define('LiveDevelopment/Agents/ConsoleAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector'],function ConsoleAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var _lastMessage; // {Console.ConsoleMessage} the last received message /** Log a remote message to the local console * @param {Console.ConsoleMessage} message */ function _log(message) { var level = message.level; if (level === "warning") { level = "warn"; } var text = "ConsoleAgent: " + message.text; if (message.stackTrace) { var callFrame = message.stackTrace[0]; text += " in " + callFrame.functionName + ":" + callFrame.columnNumber; } console[level](text); } // WebInspector Event: Console.messageAdded function _onMessageAdded(event, res) { // res = {message} _lastMessage = res.message; _log(_lastMessage); } // WebInspector Event: Console.messageRepeatCountUpdated function _onMessageRepeatCountUpdated(event, res) { // res = {count} if (_lastMessage) { _log(_lastMessage); } } // WebInspector Event: Console.messagesCleared function _onMessagesCleared(event, res) { // res = {} } /** Initialize the agent */ function load() { Inspector.Console.enable(); $(Inspector.Console) .on("messageAdded.ConsoleAgent", _onMessageAdded) .on("messageRepeatCountUpdated.ConsoleAgent", _onMessageRepeatCountUpdated) .on("messagesCleared.ConsoleAgent", _onMessagesCleared); } /** Clean up */ function unload() { $(Inspector.Console).off(".ConsoleAgent"); } // Export public functions exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * NetworkAgent tracks all resources loaded by the remote debugger. Use * `wasURLRequested(url)` to query whether a resource was loaded. */ define('LiveDevelopment/Agents/NetworkAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector'],function NetworkAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var _urlRequested; // url -> request info /** Return the URL without the query string * @param {string} URL */ function _urlWithoutQueryString(url) { var index = url.search(/[#\?]/); if (index >= 0) { url = url.substr(0, index); } return url; } /** Return the resource information for a given URL * @param {string} url */ function wasURLRequested(url) { return _urlRequested && _urlRequested[url]; } // WebInspector Event: Network.requestWillBeSent function _onRequestWillBeSent(event, res) { // res = {requestId, frameId, loaderId, documentURL, request, timestamp, initiator, stackTrace, redirectResponse} var url = _urlWithoutQueryString(res.request.url); _urlRequested[url] = true; } /** Initialize the agent */ function load() { _urlRequested = {}; Inspector.Network.enable(); $(Inspector.Network).on("requestWillBeSent.NetworkAgent", _onRequestWillBeSent); } /** Unload the agent */ function unload() { $(Inspector.Network).off(".NetworkAgent"); } // Export public functions exports.wasURLRequested = wasURLRequested; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, brackets, $, window */ /** * GotoAgent constructs and responds to the in-browser goto dialog. */ define('LiveDevelopment/Agents/GotoAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/DOMAgent','LiveDevelopment/Agents/ScriptAgent','LiveDevelopment/Agents/RemoteAgent','document/DocumentManager','editor/EditorManager'],function GotoAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); var ScriptAgent = require("LiveDevelopment/Agents/ScriptAgent"); var RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); var DocumentManager = require("document/DocumentManager"); var EditorManager = require("editor/EditorManager"); /** Return the URL without the query string * @param {string} URL */ function _urlWithoutQueryString(url) { var index = url.search(/[#\?]/); if (index >= 0) { url = url.substr(0, index); } return url; } /** Get the file component of the given url * @param {string} URL */ function _fileFromURL(url) { var comp = url.split("/"); return comp[comp.length - 1]; } /** Make the given node a target for goto * @param [] targets array * @param {DOMNode} node */ function _makeHTMLTarget(targets, node) { if (node.location) { var target = {}; var url = DOMAgent.url; var location = node.location; if (node.canHaveChildren()) { location += node.length; } url += ":" + location; var name = "<" + node.name + ">"; var file = _fileFromURL(url); targets.push({"type": "html", "url": url, "name": name, "file": file}); } } /** Make the given css rule a target for goto * @param [] targets array * @param {CSS.Rule} node */ function _makeCSSTarget(targets, rule) { if (rule.sourceURL) { var target = {}; var url = rule.sourceURL; url += ":" + rule.style.range.start; var name = rule.selectorText; var file = _fileFromURL(url); targets.push({"type": "css", "url": url, "name": name, "file": file}); } } /** Make the given javascript callFrame the target for goto * @param [] targets array * @param {Debugger.CallFrame} node */ function _makeJSTarget(targets, callFrame) { var script = ScriptAgent.scriptWithId(callFrame.location.scriptId); if (script && script.url) { var target = {}; var url = script.url; url += ":" + callFrame.location.lineNumber + "," + callFrame.location.columnNumber; var name = callFrame.functionName; if (name === "") { name = "anonymous function"; } var file = _fileFromURL(url); targets.push({"type": "js", "url": url, "name": name, "file": file}); } } /** Gather options where to go to from the given source node */ function _onRemoteShowGoto(event, res) { // res = {nodeId, name, value} var node = DOMAgent.nodeWithId(res.nodeId); // get all css rules that apply to the given node Inspector.CSS.getMatchedStylesForNode(node.nodeId, function onMatchedStyles(res) { var i, callFrame, name, script, url, rule, targets = []; _makeHTMLTarget(targets, node); for (i in node.trace) { _makeJSTarget(targets, node.trace[i]); } for (i in res.matchedCSSRules) { _makeCSSTarget(targets, res.matchedCSSRules[i]); } RemoteAgent.call("showGoto", targets); }); } /** Point the master editor to the given location * @param {integer} location in file */ function openLocation(location, noFlash) { var editor = EditorManager.getCurrentFullEditor(); var codeMirror = editor._codeMirror; if (typeof location === "number") { location = codeMirror.posFromIndex(location); } codeMirror.setCursor(location); editor.focus(); if (!noFlash) { codeMirror.setLineClass(location.line, "flash"); window.setTimeout(codeMirror.setLineClass.bind(codeMirror, location.line), 1000); } } /** Open the editor at the given url and editor location * @param {string} url * @param {integer} optional location in file */ function open(url, location, noFlash) { console.assert(url.substr(0, 7) === "file://", "Cannot open non-file URLs"); var result = new $.Deferred(); url = _urlWithoutQueryString(url); // Extract the path, also strip the third slash when on Windows var path = url.slice(brackets.platform === "win" ? 8 : 7); var promise = DocumentManager.getDocumentForPath(path); promise.done(function onDone(doc) { DocumentManager.setCurrentDocument(doc); if (location) { openLocation(location, noFlash); } result.resolve(); }); promise.fail(function onErr(err) { console.error(err); result.reject(err); }); return result.promise(); } /** Go to the given source node */ function _onRemoteGoto(event, res) { // res = {nodeId, name, value} var location, url = res.value; var matches = /^(.*):([^:]+)$/.exec(url); if (matches) { url = matches[1]; location = matches[2].split(","); if (location.length === 1) { location = parseInt(location[0], 10); } else { location = { line: parseInt(location[0], 10), ch: parseInt(location[1], 10) }; } } open(url, location); } /** Initialize the agent */ function load() { $(RemoteAgent) .on("showgoto.GotoAgent", _onRemoteShowGoto) .on("goto.GotoAgent", _onRemoteGoto); } /** Initialize the agent */ function unload() { $(RemoteAgent).off(".GotoAgent"); } // Export public functions exports.openLocation = openLocation; exports.open = open; exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $ */ /** * EditAgent propagates changes from the in-document editor to the source * document. */ define('LiveDevelopment/Agents/EditAgent',['require','exports','module','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Agents/DOMAgent','LiveDevelopment/Agents/RemoteAgent','LiveDevelopment/Agents/GotoAgent','editor/EditorManager'],function EditAgent(require, exports, module) { var Inspector = require("LiveDevelopment/Inspector/Inspector"); var DOMAgent = require("LiveDevelopment/Agents/DOMAgent"); var RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); var GotoAgent = require("LiveDevelopment/Agents/GotoAgent"); var EditorManager = require("editor/EditorManager"); /** Find changed characters * @param {string} old value * @param {string} changed value * @return {from, to, text} */ function _findChangedCharacters(oldValue, value) { if (oldValue === value) { return undefined; } var length = oldValue.length; var index = 0; // find the first character that changed var i; for (i = 0; i < length; i++) { if (value[i] !== oldValue[i]) { break; } } index += i; value = value.substr(i); length -= i; // find the last character that changed for (i = 0; i < length; i++) { if (value[value.length - 1 - i] !== oldValue[oldValue.length - 1 - i]) { break; } } length -= i; value = value.substr(0, value.length - i); return { from: index, to: index + length, text: value }; } // Remote Event: Go to the given source node function _onRemoteEdit(event, res) { // res = {nodeId, name, value} var node = DOMAgent.nodeWithId(res.nodeId); node = node.children[0]; if (!node.location) { return; } GotoAgent.open(DOMAgent.url); var editor = EditorManager.getCurrentFullEditor(); var codeMirror = editor._codeMirror; var change = _findChangedCharacters(node.value, res.value); if (change) { var from = codeMirror.posFromIndex(node.location + change.from); var to = codeMirror.posFromIndex(node.location + change.to); editor.document.replaceRange(change.text, from, to); var newPos = codeMirror.posFromIndex(node.location + change.from + change.text.length); editor.setCursorPos(newPos.line, newPos.ch); } } /** Initialize the agent */ function load() { $(RemoteAgent).on("edit.EditAgent", _onRemoteEdit); } /** Initialize the agent */ function unload() { $(RemoteAgent).off(".EditAgent"); } // Export public functions exports.load = load; exports.unload = unload; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, FileError, brackets, window */ /** * LiveDevelopment manages the Inspector, all Agents, and the active LiveDocument * * # STARTING * * To start a session call `open`. This will read the currentDocument from brackets, * launch the LiveBrowser (currently Chrome) with the remote debugger port open, * establish the Inspector connection to the remote debugger, and finally load all * agents. * * # STOPPING * * To stop a session call `close`. This will close the active browser window, * disconnect the Inspector, unload all agents, and clean up. * * # STATUS * * Status updates are dispatched as `statusChange` jQuery events. The status * codes are: * * -1: Error * 0: Inactive * 1: Connecting to the remote debugger * 2: Loading agents * 3: Active */ define('LiveDevelopment/LiveDevelopment',['require','exports','module','document/DocumentManager','editor/EditorManager','utils/NativeApp','widgets/Dialogs','strings','utils/StringUtils','LiveDevelopment/Inspector/Inspector','LiveDevelopment/Documents/HTMLDocument','LiveDevelopment/Documents/CSSDocument','LiveDevelopment/Documents/JSDocument','LiveDevelopment/Agents/ConsoleAgent','LiveDevelopment/Agents/RemoteAgent','LiveDevelopment/Agents/NetworkAgent','LiveDevelopment/Agents/DOMAgent','LiveDevelopment/Agents/CSSAgent','LiveDevelopment/Agents/ScriptAgent','LiveDevelopment/Agents/HighlightAgent','LiveDevelopment/Agents/GotoAgent','LiveDevelopment/Agents/EditAgent'],function LiveDevelopment(require, exports, module) { // Status Codes var STATUS_ERROR = exports.STATUS_ERROR = -1; var STATUS_INACTIVE = exports.STATUS_INACTIVE = 0; var STATUS_CONNECTING = exports.STATUS_CONNECTING = 1; var STATUS_LOADING_AGENTS = exports.STATUS_LOADING_AGENTS = 2; var STATUS_ACTIVE = exports.STATUS_ACTIVE = 3; var DocumentManager = require("document/DocumentManager"); var EditorManager = require("editor/EditorManager"); var NativeApp = require("utils/NativeApp"); var Dialogs = require("widgets/Dialogs"); var Strings = require("strings"); var StringUtils = require("utils/StringUtils"); // Inspector var Inspector = require("LiveDevelopment/Inspector/Inspector"); // Documents var HTMLDocument = require("LiveDevelopment/Documents/HTMLDocument"); var CSSDocument = require("LiveDevelopment/Documents/CSSDocument"); var JSDocument = require("LiveDevelopment/Documents/JSDocument"); // Agents var agents = { "console": require("LiveDevelopment/Agents/ConsoleAgent"), "remote": require("LiveDevelopment/Agents/RemoteAgent"), "network": require("LiveDevelopment/Agents/NetworkAgent"), "dom": require("LiveDevelopment/Agents/DOMAgent"), "css": require("LiveDevelopment/Agents/CSSAgent"), "script": require("LiveDevelopment/Agents/ScriptAgent"), "highlight": require("LiveDevelopment/Agents/HighlightAgent"), "goto": require("LiveDevelopment/Agents/GotoAgent"), "edit": require("LiveDevelopment/Agents/EditAgent") }; // Some agents are still experimental, so we don't enable them all by default // However, extensions can enable them by calling enableAgent(). // This object is used as a set (thus all properties have the value 'true'). // Property names should match property names in the 'agents' object. var _enabledAgentNames = { "console": true, "remote": true, "network": true, "dom": true, "css": true }; // store the names (matching property names in the 'agent' object) of agents that we've loaded var _loadedAgentNames = []; var _htmlDocumentPath; // the path of the html file open for live development var _liveDocument; // the document open for live editing. var _relatedDocuments; // CSS and JS documents that are used by the live HTML document /** Augments the given Brackets document with information that's useful for live development. */ function _setDocInfo(doc) { // FUTURE: some of these things should just be moved into core Document; others should // be in a LiveDevelopment-specific object attached to the doc. var matches = /^(.*\/)(.+\.([^.]+))$/.exec(doc.file.fullPath); if (matches) { var prefix = "file://"; // The file.fullPath on Windows starts with a drive letter ("C:"). // In order to make it a valid file: URL we need to add an // additional slash to the prefix. if (brackets.platform === "win") { prefix += "/"; } doc.extension = matches[3]; doc.url = encodeURI(prefix + doc.file.fullPath); // the root represents the document that should be displayed in the browser // for live development (the file for HTML files, index.html for others) var fileName = /^html?$/.test(matches[3]) ? matches[2] : "index.html"; doc.root = {url: encodeURI(prefix + matches[1] + fileName)}; } } /** Get the current document from the document manager * _adds extension, url and root to the document */ function _getCurrentDocument() { var doc = DocumentManager.getCurrentDocument(); if (doc) { _setDocInfo(doc); } return doc; } /** Determine which document class should be used for a given document * @param {Document} document */ function _classForDocument(doc) { switch (doc.extension) { case "css": return CSSDocument; /* FUTURE: case "js": return JSDocument; case "html": case "htm": return HTMLDocument; default: throw "Invalid document type: " + doc.extension; */ } return null; } /** * Removes the given CSS/JSDocument from _relatedDocuments. Signals that the * given file is no longer associated with the HTML document that is live (e.g. * if the related file has been deleted on disk). */ function _handleRelatedDocumentDeleted(event, liveDoc) { var index = _relatedDocuments.indexOf(liveDoc); if (index !== -1) { $(liveDoc).on("deleted", _handleRelatedDocumentDeleted); _relatedDocuments.splice(index, 1); } } /** Close a live document */ function _closeDocument() { if (_liveDocument) { _liveDocument.close(); _liveDocument = undefined; } if (_relatedDocuments) { _relatedDocuments.forEach(function (liveDoc) { liveDoc.close(); $(liveDoc).off("deleted", _handleRelatedDocumentDeleted); }); _relatedDocuments = undefined; } } /** Create a live version of a Brackets document */ function _createDocument(doc, editor) { var DocClass = _classForDocument(doc); if (DocClass) { return new DocClass(doc, editor); } else { return null; } } /** Convert a file: URL to a local full file path */ function _urlToPath(url) { var path; if (url.indexOf("file://") === 0) { path = url.slice(7); if (path && brackets.platform === "win" && path.charAt(0) === "/") { path = path.slice(1); } } return decodeURI(path); } /** Open a live document * @param {Document} source document to open */ function _openDocument(doc, editor) { _closeDocument(); _liveDocument = _createDocument(doc, editor); // Gather related CSS documents. // FUTURE: Gather related JS documents as well. _relatedDocuments = []; agents.css.getStylesheetURLs().forEach(function (url) { // FUTURE: when we get truly async file handling, we might need to prevent other // stuff from happening while we wait to add these listeners DocumentManager.getDocumentForPath(_urlToPath(url)) .done(function (doc) { _setDocInfo(doc); var liveDoc = _createDocument(doc); if (liveDoc) { _relatedDocuments.push(liveDoc); $(liveDoc).on("deleted", _handleRelatedDocumentDeleted); } }); }); } /** Unload the agents */ function unloadAgents() { _loadedAgentNames.forEach(function (name) { agents[name].unload(); }); _loadedAgentNames = []; } /** Load the agents */ function loadAgents() { var name, promises = []; for (name in _enabledAgentNames) { if (_enabledAgentNames.hasOwnProperty(name) && agents[name].load) { promises.push(agents[name].load()); _loadedAgentNames.push(name); } } return promises; } /** Enable an agent. Takes effect next time a connection is made. Does not affect * current live development sessions. * * @param {string} name of agent to enable */ function enableAgent(name) { if (agents.hasOwnProperty(name) && !_enabledAgentNames.hasOwnProperty(name)) { _enabledAgentNames[name] = true; } } /** Disable an agent. Takes effect next time a connection is made. Does not affect * current live development sessions. * * @param {string} name of agent to disable */ function disableAgent(name) { if (_enabledAgentNames.hasOwnProperty(name)) { delete _enabledAgentNames[name]; } } /** Update the status * @param {integer} new status */ function _setStatus(status) { exports.status = status; $(exports).triggerHandler("statusChange", status); } /** Triggered by Inspector.error */ function _onError(event, error) { var message = error.message; // Additional information, like exactly which parameter could not be processed. var data = error.data; if (Array.isArray(data)) { message += "\n" + data.join("\n"); } // Show the message, but include the error object for further information (e.g. error code) console.error(message, error); } /** Run when all agents are loaded */ function _onLoad() { var doc = _getCurrentDocument(); if (doc) { var editor = EditorManager.getCurrentFullEditor(); _openDocument(doc, editor); } _setStatus(STATUS_ACTIVE); } /** Triggered by Inspector.connect */ function _onConnect(event) { var promises = loadAgents(); _setStatus(STATUS_LOADING_AGENTS); $.when.apply(undefined, promises).then(_onLoad, _onError); } /** Triggered by Inspector.disconnect */ function _onDisconnect(event) { unloadAgents(); _closeDocument(); _setStatus(STATUS_INACTIVE); } /** Open the Connection and go live */ function open() { var result = new $.Deferred(), promise = result.promise(); var doc = _getCurrentDocument(); var browserStarted = false; var retryCount = 0; function showWrongDocError() { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.LIVE_DEVELOPMENT_ERROR_TITLE, Strings.LIVE_DEV_NEED_HTML_MESSAGE ); result.reject("WRONG_DOC"); } if (!doc || !doc.root) { showWrongDocError(); } else { // For Sprint 6, we only open live development connections for HTML files // FUTURE: Remove this test when we support opening connections for different // file types. if (!doc.extension || doc.extension.indexOf("htm") !== 0) { showWrongDocError(); return promise; } _setStatus(STATUS_CONNECTING); Inspector.connectToURL(doc.root.url).then(result.resolve, function onConnectFail(err) { if (err === "CANCEL") { result.reject(err); return; } if (retryCount > 6) { _setStatus(STATUS_ERROR); Dialogs.showModalDialog( Dialogs.DIALOG_ID_LIVE_DEVELOPMENT, Strings.LIVE_DEVELOPMENT_ERROR_TITLE, Strings.LIVE_DEVELOPMENT_ERROR_MESSAGE ).done(function (id) { if (id === Dialogs.DIALOG_BTN_OK) { // User has chosen to reload Chrome, quit the running instance _setStatus(STATUS_INACTIVE); NativeApp.closeLiveBrowser() .done(function () { browserStarted = false; window.setTimeout(function () { open().then(result.resolve, result.reject); }); }) .fail(function (err) { // Report error? _setStatus(STATUS_ERROR); browserStarted = false; result.reject("CLOSE_LIVE_BROWSER"); }); } else { result.reject("CANCEL"); } }); return; } retryCount++; if (!browserStarted && exports.status !== STATUS_ERROR) { // If err === FileError.ERR_NOT_FOUND, it means a remote debugger connection // is available, but the requested URL is not loaded in the browser. In that // case we want to launch the live browser (to open the url in a new tab) // without using the --remote-debugging-port flag. This works around issues // on Windows where Chrome can't be opened more than once with the // --remote-debugging-port flag set. NativeApp.openLiveBrowser( doc.root.url, err !== FileError.ERR_NOT_FOUND ) .done(function () { browserStarted = true; }) .fail(function (err) { var message; _setStatus(STATUS_ERROR); if (err === FileError.NOT_FOUND_ERR) { message = Strings.ERROR_CANT_FIND_CHROME; } else { message = StringUtils.format(Strings.ERROR_LAUNCHING_BROWSER, err); } Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_LAUNCHING_BROWSER_TITLE, message ); result.reject("OPEN_LIVE_BROWSER"); }); } if (exports.status !== STATUS_ERROR) { window.setTimeout(function retryConnect() { Inspector.connectToURL(doc.root.url).then(result.resolve, onConnectFail); }, 500); } }); } return promise; } /** Close the Connection */ function close() { if (Inspector.connected()) { Inspector.Runtime.evaluate("window.close()"); } Inspector.disconnect(); _setStatus(STATUS_INACTIVE); } /** Triggered by a document change from the DocumentManager */ function _onDocumentChange() { var doc = _getCurrentDocument(); if (!doc) { return; } if (Inspector.connected()) { if (agents.network && agents.network.wasURLRequested(doc.url)) { _closeDocument(); var editor = EditorManager.getCurrentFullEditor(); _openDocument(doc, editor); } else { /* FUTURE: support live connections for docments other than html */ if (doc.extension && doc.extension.indexOf("htm") === 0 && doc.file.fullPath !== _htmlDocumentPath) { close(); window.setTimeout(open); _htmlDocumentPath = doc.file.fullPath; } } } else if (exports.config.autoconnect) { window.setTimeout(open); } } function getLiveDocForPath(path) { var docsToSearch = []; if (_relatedDocuments) { docsToSearch = docsToSearch.concat(_relatedDocuments); } if (_liveDocument) { docsToSearch = docsToSearch.concat(_liveDocument); } var foundDoc; docsToSearch.some(function matchesPath(ele) { if (ele.doc.file.fullPath === path) { foundDoc = ele; return true; } return false; }); return foundDoc; } /** Hide any active highlighting */ function hideHighlight() { if (Inspector.connected() && agents.highlight) { agents.highlight.hide(); } } /** Initialize the LiveDevelopment Session */ function init(theConfig) { exports.config = theConfig; $(Inspector).on("connect", _onConnect) .on("disconnect", _onDisconnect) .on("error", _onError); $(DocumentManager).on("currentDocumentChange", _onDocumentChange); } // Export public functions exports.agents = agents; exports.open = open; exports.close = close; exports.enableAgent = enableAgent; exports.disableAgent = disableAgent; exports.getLiveDocForPath = getLiveDocForPath; exports.hideHighlight = hideHighlight; exports.init = init; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, forin: true, maxerr: 50, regexp: true */ /*global define, $, less, window, XMLHttpRequest */ /** * main integrates LiveDevelopment into Brackets * * This module creates two menu items: * * "Go Live": open or close a Live Development session and visualize the status * "Highlight": toggle source highlighting * * @require DocumentManager */ define('LiveDevelopment/main',['require','exports','module','document/DocumentManager','command/Commands','LiveDevelopment/LiveDevelopment','LiveDevelopment/Inspector/Inspector','command/CommandManager','strings'],function main(require, exports, module) { var DocumentManager = require("document/DocumentManager"), Commands = require("command/Commands"), LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), Inspector = require("LiveDevelopment/Inspector/Inspector"), CommandManager = require("command/CommandManager"), Strings = require("strings"); var config = { debug: true, // enable debug output and helpers autoconnect: false, // go live automatically after startup? highlight: false, // enable highlighting? highlightConfig: { // the highlight configuration for the Inspector borderColor: {r: 255, g: 229, b: 153, a: 0.66}, contentColor: {r: 111, g: 168, b: 220, a: 0.55}, marginColor: {r: 246, g: 178, b: 107, a: 0.66}, paddingColor: {r: 147, g: 196, b: 125, a: 0.66}, showInfo: true } }; var _checkMark = "✓"; // Check mark character // Status labels/styles are ordered: error, not connected, progress1, progress2, connected. var _statusTooltip = [Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, Strings.LIVE_DEV_STATUS_TIP_NOT_CONNECTED, Strings.LIVE_DEV_STATUS_TIP_PROGRESS1, Strings.LIVE_DEV_STATUS_TIP_PROGRESS2, Strings.LIVE_DEV_STATUS_TIP_CONNECTED]; // Status indicator tooltip var _statusStyle = ["warning", "", "info", "info", "success"]; // Status indicator's CSS class var _allStatusStyles = _statusStyle.join(" "); var _$btnGoLive; // reference to the GoLive button var _$btnHighlight; // reference to the HighlightButton /** Load Live Development LESS Style */ function _loadStyles() { var request = new XMLHttpRequest(); request.open("GET", "LiveDevelopment/main.less", true); request.onload = function onLoad(event) { var parser = new less.Parser(); parser.parse(request.responseText, function onParse(err, tree) { console.assert(!err, err); $("<style>" + tree.toCSS() + "</style>") .appendTo(window.document.head); }); }; request.send(null); } /** * Change the appearance of a button. Omit text to remove any extra text; omit style to return to default styling; * omit tooltip to leave tooltip unchanged. */ function _setLabel($btn, text, style, tooltip) { // Clear text/styles from previous status $("span", $btn).remove(); $btn.removeClass(_allStatusStyles); // Set text/styles for new status if (text && text.length > 0) { $("<span class=\"label\">") .addClass(style) .text(text) .appendTo($btn); } else { $btn.addClass(style); } if (tooltip) { $btn.attr("title", tooltip); } } /** Toggles LiveDevelopment and synchronizes the state of UI elements that reports LiveDevelopment status */ function _handleGoLiveCommand() { if (LiveDevelopment.status >= LiveDevelopment.STATUS_CONNECTING) { LiveDevelopment.close(); // TODO Ty: when checkmark support lands, remove checkmark } else { LiveDevelopment.open(); // TODO Ty: when checkmark support lands, add checkmark } } /** Create the menu item "Go Live" */ function _setupGoLiveButton() { _$btnGoLive = $("#toolbar-go-live"); _$btnGoLive.click(function onGoLive() { _handleGoLiveCommand(); }); $(LiveDevelopment).on("statusChange", function statusChange(event, status) { // status starts at -1 (error), so add one when looking up name and style // See the comments at the top of LiveDevelopment.js for details on the // various status codes. _setLabel(_$btnGoLive, null, _statusStyle[status + 1], _statusTooltip[status + 1]); }); // Initialize tooltip for 'not connected' state _setLabel(_$btnGoLive, null, _statusStyle[1], _statusTooltip[1]); } /** Create the menu item "Highlight" */ function _setupHighlightButton() { // TODO: this should be moved into index.html like the Go Live button once it's re-enabled _$btnHighlight = $("<a href=\"#\">Highlight </a>"); $(".nav").append($("<li>").append(_$btnHighlight)); _$btnHighlight.click(function onClick() { config.highlight = !config.highlight; if (config.highlight) { _setLabel(_$btnHighlight, _checkMark, "success"); } else { _setLabel(_$btnHighlight); LiveDevelopment.hideHighlight(); } }); if (config.highlight) { _setLabel(_$btnHighlight, _checkMark, "success"); } } /** Setup window references to useful LiveDevelopment modules */ function _setupDebugHelpers() { window.ld = LiveDevelopment; window.i = Inspector; window.report = function report(params) { window.params = params; console.info(params); }; } /** Initialize LiveDevelopment */ function init() { Inspector.init(config); LiveDevelopment.init(config); _loadStyles(); _setupGoLiveButton(); /* _setupHighlightButton(); FUTURE - Highlight button */ if (config.debug) { _setupDebugHelpers(); } } window.setTimeout(init); CommandManager.register(Strings.CMD_LIVE_FILE_PREVIEW, Commands.FILE_LIVE_FILE_PREVIEW, _handleGoLiveCommand); // Export public functions exports.init = init; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, PathUtils */ /* * Manages a collection of FileIndexes where each index maintains a list of information about * files that meet the criteria specified by the index. The indexes are created lazily when * they are queried and marked dirty when Brackets becomes active. * * TODO (issue 325 ) - FileIndexer doesn't currently add a file to the index when the user createa * a new file within brackets. * */ define('project/FileIndexManager',['require','exports','module','file/NativeFileSystem','utils/PerfUtils','project/ProjectManager','widgets/Dialogs','strings'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PerfUtils = require("utils/PerfUtils"), ProjectManager = require("project/ProjectManager"), Dialogs = require("widgets/Dialogs"), Strings = require("strings"); /** * All the indexes are stored in this object. The key is the name of the index * and the value is a FileIndex. */ var _indexList = {}; /** * Tracks whether _indexList should be considered dirty and invalid. Calls that access * any data in _indexList should call syncFileIndex prior to accessing the data. * @type {boolean} */ var _indexListDirty = true; /** class FileIndex * * A FileIndex contains an array of fileInfos that meet the criteria specified by * the filterFunction. FileInfo's in the fileInfo array should unique map to one file. * * @constructor * @param {!string} indexname * @param {function({!entry})} filterFunction returns true to indicate the entry * should be included in the index */ function FileIndex(indexName, filterFunction) { this.name = indexName; this.fileInfos = []; this.filterFunction = filterFunction; } /** class FileInfo * * Class to hold info about a file that a FileIndex wishes to retain. * * @constructor * @param {!string} */ function FileInfo(entry) { this.name = entry.name; this.fullPath = entry.fullPath; } /** * Adds a new index to _indexList and marks the list dirty * * A future performance optimization is to only build the new index rather than * marking them all dirty * * @private * @param {!string} indexName must be unque * @param {!function({entry} filterFunction should return true to include an * entry in the index */ function _addIndex(indexName, filterFunction) { if (_indexList.hasOwnProperty(indexName)) { throw new Error("Duplicate index name"); } if (typeof filterFunction !== "function") { throw new Error("Invalid arguments"); } _indexList[indexName] = new FileIndex(indexName, filterFunction); _indexListDirty = true; } /** * Checks the entry against the filterFunction for each index and adds * a fileInfo to the index if the entry meets the criteria. FileInfo's are * shared between indexes. * * @private * @param {!entry} entry to be added to the indexes */ // future use when files are incrementally added // function _addFileToIndexes(entry) { // skip invisible files if (!ProjectManager.shouldShow(entry)) { return; } var fileInfo = new FileInfo(entry); //console.log(entry.name); $.each(_indexList, function (indexName, index) { if (index.filterFunction(entry)) { index.fileInfos.push(fileInfo); } }); } /** * Error dialog when max files in index is hit */ function _showMaxFilesDialog() { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_MAX_FILES_TITLE, Strings.ERROR_MAX_FILES ); } /* Recursively visits all files that are descendent of dirEntry and adds * files files to each index when the file matches the filter critera * @private * @param {!DirectoryEntry} dirEntry * @returns {$.Promise} */ function _scanDirectorySubTree(dirEntry) { if (!dirEntry) { throw new Error("Bad dirEntry passed to _scanDirectorySubTree"); } // keep track of directories as they are asynchronously read. We know we are done // when dirInProgress becomes empty again. var state = { fileCount: 0, dirInProgress: {}, // directory names that are in progress of being read dirError: {}, // directory names with read errors. key=dir path, value=error maxFilesHit: false // used to show warning dialog only once }; var deferred = new $.Deferred(); // inner helper function function _dirScanDone() { var key; for (key in state.dirInProgress) { if (state.dirInProgress.hasOwnProperty(key)) { return false; } } return true; } function _finishDirScan(dirEntry) { //console.log("finished: " + dirEntry.fullPath); delete state.dirInProgress[dirEntry.fullPath]; if (_dirScanDone()) { //console.log("dir scan completly done"); deferred.resolve(); } } // inner helper function function _scanDirectoryRecurse(dirEntry) { // skip invisible directories if (!ProjectManager.shouldShow(dirEntry)) { return; } state.dirInProgress[dirEntry.fullPath] = true; //console.log("started dir: " + dirEntry.fullPath); dirEntry.createReader().readEntries( // success callback function (entries) { // inspect all children of dirEntry entries.forEach(function (entry) { // For now limit the number of files that are indexed by preventing adding files // or scanning additional directories once a max has been hit. Also notify the // user once via a dialog. This limit could be increased // if files were indexed in a worker thread so scanning didn't block the UI if (state.fileCount > 10000) { if (!state.maxFilesHit) { state.maxFilesHit = true; _showMaxFilesDialog(); } return; } if (entry.isFile) { _addFileToIndexes(entry); state.fileCount++; } else if (entry.isDirectory) { _scanDirectoryRecurse(entry); } }); _finishDirScan(dirEntry); }, // error callback function (error) { state.dirError[dirEntry.fullPath] = error; _finishDirScan(dirEntry); } ); } _scanDirectoryRecurse(dirEntry); return deferred.promise(); } // debug function _logFileList(list) { list.forEach(function (fileInfo) { console.log(fileInfo.name); }); console.log("length: " + list.length); } /** * Clears the fileInfo array for all the indexes in _indexList * @private */ function _clearIndexes() { $.each(_indexList, function (indexName, index) { index.fileInfos = []; }); } /** * Markes all file indexes dirty */ function markDirty() { _indexListDirty = true; } /** * Used by syncFileIndex function to prevent reentrancy * @private */ var _syncFileIndexReentracyGuard = false; /** * Clears and rebuilds all of the fileIndexes and sets _indexListDirty to false * @return {$.Promise} resolved when index has been updated */ function syncFileIndex() { // TODO (issue 330) - allow multiple calls to syncFileIndex to be batched up so that this code isn't necessary if (_syncFileIndexReentracyGuard) { throw new Error("syncFileIndex cannot be called Recursively"); } _syncFileIndexReentracyGuard = true; var rootDir = ProjectManager.getProjectRoot(); if (_indexListDirty) { PerfUtils.markStart(PerfUtils.FILE_INDEX_MANAGER_SYNC); _clearIndexes(); return _scanDirectorySubTree(rootDir) .done(function () { PerfUtils.addMeasurement(PerfUtils.FILE_INDEX_MANAGER_SYNC); _indexListDirty = false; _syncFileIndexReentracyGuard = false; //_logFileList(_indexList["all"].fileInfos); //_logFileList(_indexList["css"].fileInfos); }); } else { _syncFileIndexReentracyGuard = false; return $.Deferred().resolve().promise(); } } /** * Returns the FileInfo array for the specified index * @param {!string} indexname * @return {$.Promise} a promise that is resolved with an Array of FileInfo's */ function getFileInfoList(indexName) { var result = new $.Deferred(); if (!_indexList.hasOwnProperty(indexName)) { throw new Error("indexName not found"); } syncFileIndex() .done(function () { result.resolve(_indexList[indexName].fileInfos); }); return result.promise(); } /** * Calls the filterFunction on every in the index specified by indexName * and return a a new list of FileInfo's * @param {!string} * @param {function({string})} filterFunction * @return {$.Promise} a promise that is resolved with an Array of FileInfo's */ function getFilteredList(indexName, filterFunction) { var result = new $.Deferred(); if (!_indexList.hasOwnProperty(indexName)) { throw new Error("indexName not found"); } syncFileIndex() .done(function () { var resultList = []; getFileInfoList(indexName) .done(function (fileList) { resultList = fileList.filter(function (fileInfo) { return filterFunction(fileInfo.name); }); result.resolve(resultList); }); }); return result.promise(); } /** * returns an array of fileInfo's that match the filename parameter * @param {!string} indexName * @param {!filename} * @return {$.Promise} a promise that is resolved with an Array of FileInfo's */ function getFilenameMatches(indexName, filename) { return getFilteredList(indexName, function (item) { return item === filename; }); } /** * Add the indexes */ _addIndex( "all", function (entry) { return true; } ); _addIndex( "css", function (entry) { var filename = entry.name; return PathUtils.filenameExtension(filename) === ".css"; } ); $(ProjectManager).on("projectOpen", function (event, projectRoot) { markDirty(); }); PerfUtils.createPerfMeasurement("FILE_INDEX_MANAGER_SYNC", "syncFileIndex"); exports.markDirty = markDirty; exports.getFileInfoList = getFileInfoList; exports.getFilenameMatches = getFilenameMatches; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, _parseRuleList: true */ // JSLint Note: _parseRuleList() is cyclical dependency, not a global function. // It was added to this list to prevent JSLint warning about being used before being defined. /** * Set of utilities for simple parsing of CSS text. */ define('language/CSSUtils',['require','exports','module','utils/Async','document/DocumentManager','editor/EditorManager','language/HTMLUtils','project/FileIndexManager','file/NativeFileSystem'],function (require, exports, module) { var Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), HTMLUtils = require("language/HTMLUtils"), FileIndexManager = require("project/FileIndexManager"), NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem; /** * Extracts all CSS selectors from the given text * Returns an array of selectors. Each selector is an object with the following properties: selector: the text of the selector (note: comma separated selector groups like "h1, h2" are broken into separate selectors) ruleStartLine: line in the text where the rule (including preceding comment) appears ruleStartChar: column in the line where the rule (including preceding comment) starts selectorStartLine: line in the text where the selector appears selectorStartChar: column in the line where the selector starts selectorEndLine: line where the selector ends selectorEndChar: column where the selector ends selectorGroupStartLine: line where the comma-separated selector group (e.g. .foo, .bar, .baz) starts that this selector (e.g. .baz) is part of. Particularly relevant for groups that are on multiple lines. selectorGroupStartChar: column in line where the selector group starts. declListStartLine: line where the declaration list for the rule starts declListStartChar: column in line where the declaration list for the rule starts declListEndLine: line where the declaration list for the rule ends declListEndChar: column in the line where the declaration list for the rule ends * @param text {!String} CSS text to extract from * @return {Array.<Object>} Array with objects specifying selectors. */ function extractAllSelectors(text) { var selectors = []; var mode = CodeMirror.getMode({indentUnit: 2}, "css"); var state, lines, lineCount; var token, style, stream, line; var currentSelector = ""; var ruleStartChar = -1, ruleStartLine = -1; var selectorStartChar = -1, selectorStartLine = -1; var selectorGroupStartLine = -1, selectorGroupStartChar = -1; var declListStartLine = -1, declListStartChar = -1; // implement _firstToken()/_nextToken() methods to // provide a single stream of tokens function _hasStream() { while (stream.eol()) { line++; if (line >= lineCount) { return false; } if (currentSelector.match(/\S/)) { // If we are in a current selector and starting a newline, // make sure there is whitespace in the selector currentSelector += " "; } stream = new CodeMirror.StringStream(lines[line]); } return true; } function _firstToken() { state = CodeMirror.startState(mode); lines = CodeMirror.splitLines(text); lineCount = lines.length; if (lineCount === 0) { return false; } line = 0; stream = new CodeMirror.StringStream(lines[line]); if (!_hasStream()) { return false; } style = mode.token(stream, state); token = stream.current(); return true; } function _nextToken() { // advance the stream past this token stream.start = stream.pos; if (!_hasStream()) { return false; } style = mode.token(stream, state); token = stream.current(); return true; } function _firstTokenSkippingWhitespace() { if (!_firstToken()) { return false; } while (!token.match(/\S/)) { if (!_nextToken()) { return false; } } return true; } function _nextTokenSkippingWhitespace() { if (!_nextToken()) { return false; } while (!token.match(/\S/)) { if (!_nextToken()) { return false; } } return true; } function _isStartComment() { return (token.match(/^\/\*/)); } function _parseComment() { while (!token.match(/\*\/$/)) { if (!_nextToken()) { break; } } } function _nextTokenSkippingComments() { if (!_nextToken()) { return false; } while (_isStartComment()) { _parseComment(); if (!_nextToken()) { return false; } } return true; } function _parseSelector() { currentSelector = ""; selectorStartChar = stream.start; selectorStartLine = line; // Everything until the next ',' or '{' is part of the current selector while (token !== "," && token !== "{") { currentSelector += token; if (!_nextTokenSkippingComments()) { break; } } currentSelector = currentSelector.trim(); if (currentSelector !== "") { selectors.push({selector: currentSelector, ruleStartLine: ruleStartLine, ruleStartChar: ruleStartChar, selectorStartLine: selectorStartLine, selectorStartChar: selectorStartChar, declListEndLine: -1, selectorEndLine: line, selectorEndChar: stream.start - 1, // stream.start points to the first char of the non-selector token selectorGroupStartLine: selectorGroupStartLine, selectorGroupStartChar: selectorGroupStartChar }); currentSelector = ""; } selectorStartChar = -1; } function _parseSelectorList() { selectorGroupStartLine = line; selectorGroupStartChar = stream.start; _parseSelector(); while (token === ",") { if (!_nextTokenSkippingComments()) { break; } _parseSelector(); } } function _parseDeclarationList() { var j; declListStartLine = line; declListStartChar = stream.start; // Since we're now in a declaration list, that means we also finished // parsing the whole selector group. Therefore, reset selectorGroupStartLine // so that next time we parse a selector we know it's a new group selectorGroupStartLine = -1; selectorGroupStartChar = -1; ruleStartLine = -1; ruleStartChar = -1; // Skip everything until the next '}' while (token !== "}") { if (!_nextTokenSkippingComments()) { break; } } // assign this declaration list position to every selector on the stack // that doesn't have a declaration list start and end line for (j = selectors.length - 1; j >= 0; j--) { if (selectors[j].declListEndLine !== -1) { break; } else { selectors[j].declListStartLine = declListStartLine; selectors[j].declListStartChar = declListStartChar; selectors[j].declListEndLine = line; selectors[j].declListEndChar = stream.pos - 1; // stream.pos actually points to the char after the } } } } function includeCommentInNextRule() { if (ruleStartChar !== -1) { return false; // already included } if (stream.start > 0 && lines[line].substr(0, stream.start).indexOf("}") !== -1) { return false; // on same line as '}', so it's for previous rule } return true; } function _isStartAtRule() { return (token.match(/^@/)); } function _parseAtRule() { // reset these fields to ignore comments preceding @rules ruleStartLine = -1; ruleStartChar = -1; selectorStartLine = -1; selectorStartChar = -1; selectorGroupStartLine = -1; selectorGroupStartChar = -1; if (token.match(/@media/i)) { // @media rule holds a rule list // Skip everything until the opening '{' while (token !== "{") { if (!_nextTokenSkippingComments()) { break; } } _nextTokenSkippingWhitespace(); // skip past '{', to next non-ws token // Parse rules until we see '}' _parseRuleList("}"); } else if (token.match(/@(charset|import|namespace)/i)) { // This code handles @rules in this format: // @rule ... ; // Skip everything until the next ';' while (token !== ";") { if (!_nextTokenSkippingComments()) { break; } } } else { // This code handle @rules that use this format: // @rule ... { ... } // such as @page, @keyframes (also -webkit-keyframes, etc.), and @font-face. // Skip everything until the next '}' while (token !== "}") { if (!_nextTokenSkippingComments()) { break; } } } } // parse a style rule function _parseRule() { _parseSelectorList(); _parseDeclarationList(); } function _parseRuleList(escapeToken) { while ((!escapeToken) || token !== escapeToken) { if (_isStartAtRule()) { // @rule _parseAtRule(); } else if (_isStartComment()) { // comment - make this part of style rule if (includeCommentInNextRule()) { ruleStartChar = stream.start; ruleStartLine = line; } _parseComment(); } else { // Otherwise, it's style rule if (ruleStartChar === -1) { ruleStartChar = stream.start; ruleStartLine = line; } _parseRule(); } if (!_nextTokenSkippingWhitespace()) { break; } } } // Do parsing if (_firstTokenSkippingWhitespace()) { // Style sheet is a rule list _parseRuleList(); } return selectors; } /* * This code can be used to create an "independent" HTML document that can be passed to jQuery * calls. Allows using jQuery's CSS selector engine without actually putting anything in the browser's DOM * var _htmlDoctype = document.implementation.createDocumentType('html', '-//W3C//DTD XHTML 1.0 Strict//EN', 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' ); var _htmlDocument = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', _htmlDoctype); function checkIfSelectorSelectsHTML(selector, theHTML) { $('html', _htmlDocument).html(theHTML); return ($(selector, _htmlDocument).length > 0); } */ /** * Finds all instances of the specified selector in "text". * Returns an Array of Objects with start and end properties. * * For Sprint 4, we only support simple selectors. This function will need to change * dramatically to support full selectors. * * FUTURE: (JRB) It would be nice to eventually use the browser/jquery to do the selector evaluation. * One way to do this would be to take the user's HTML, add a special attribute to every tag with a UID, * and then construct a DOM (using the commented out code above). Then, give this DOM and the selector to * jquery and ask what matches. If the node that the user's cursor is in comes back from jquery, then * we know the selector applies. * * @param text {!String} CSS text to search * @param selector {!String} selector to search for * @return {Array.<{selectorGroupStartLine:number, declListEndLine:number, selector:string}>} * Array of objects containing the start and end line numbers (0-based, inclusive range) for each * matched selector. */ function _findAllMatchingSelectorsInText(text, selector) { var allSelectors = extractAllSelectors(text); var result = []; var i; // For sprint 4 we only match the rightmost simple selector, and ignore // attribute selectors and pseudo selectors var classOrIdSelector = selector[0] === "." || selector[0] === "#"; var prefix = ""; // Escape initial "." in selector, if present. if (selector[0] === ".") { selector = "\\" + selector; } if (!classOrIdSelector) { // Tag selectors must have nothing or whitespace before it. selector = "(^|\\s)" + selector; } var re = new RegExp(selector + "(\\[[^\\]]*\\]|:{1,2}[\\w-()]+|\\.[\\w-]+|#[\\w-]+)*\\s*$", classOrIdSelector ? "" : "i"); allSelectors.forEach(function (entry) { if (entry.selector.search(re) !== -1) { result.push(entry); } else if (!classOrIdSelector) { // Special case for tag selectors - match "*" as the rightmost character if (entry.selector.trim().search(/\*$/) !== -1) { result.push(entry); } } }); return result; } /** * Converts the results of _findAllMatchingSelectorsInText() into a simpler bag of data and * appends those new objects to the given 'resultSelectors' Array. * @param {Array.<{document:Document, lineStart:number, lineEnd:number}>} resultSelectors * @param {Array.<{selectorGroupStartLine:number, declListEndLine:number, selector:string}>} selectorsToAdd * @param {!Document} sourceDoc * @param {!number} lineOffset Amount to offset all line number info by. Used if the first line * of the parsed CSS text is not the first line of the sourceDoc. */ function _addSelectorsToResults(resultSelectors, selectorsToAdd, sourceDoc, lineOffset) { selectorsToAdd.forEach(function (selectorInfo) { resultSelectors.push({ name: selectorInfo.selector, document: sourceDoc, lineStart: selectorInfo.ruleStartLine + lineOffset, lineEnd: selectorInfo.declListEndLine + lineOffset }); }); } /** Finds matching selectors in CSS files; adds them to 'resultSelectors' */ function _findMatchingRulesInCSSFiles(selector, resultSelectors) { var result = new $.Deferred(), cssFilesResult = FileIndexManager.getFileInfoList("css"); // Load one CSS file and search its contents function _loadFileAndScan(fullPath, selector) { var oneFileResult = new $.Deferred(); DocumentManager.getDocumentForPath(fullPath) .done(function (doc) { // Find all matching rules for the given CSS file's content, and add them to the // overall search result var oneCSSFileMatches = _findAllMatchingSelectorsInText(doc.getText(), selector); _addSelectorsToResults(resultSelectors, oneCSSFileMatches, doc, 0); oneFileResult.resolve(); }) .fail(function (error) { oneFileResult.reject(error); }); return oneFileResult.promise(); } // Load index of all CSS files; then process each CSS file in turn (see above) cssFilesResult.done(function (fileInfos) { Async.doInParallel(fileInfos, function (fileInfo, number) { return _loadFileAndScan(fileInfo.fullPath, selector); }) .pipe(result.resolve, result.reject); }); return result.promise(); } /** Finds matching selectors in the <style> block of a single HTML file; adds them to 'resultSelectors' */ function _findMatchingRulesInStyleBlocks(htmlDocument, selector, resultSelectors) { // HTMLUtils requires a real CodeMirror instance; make sure we can give it the right Editor var htmlEditor = EditorManager.getCurrentFullEditor(); if (htmlEditor.document !== htmlDocument) { console.error("Cannot search for <style> blocks in HTML file other than current editor"); return; } // Find all <style> blocks in the HTML file var styleBlocks = HTMLUtils.findStyleBlocks(htmlEditor); styleBlocks.forEach(function (styleBlockInfo) { // Search this one <style> block's content, appending results to 'resultSelectors' var oneStyleBlockMatches = _findAllMatchingSelectorsInText(styleBlockInfo.text, selector); _addSelectorsToResults(resultSelectors, oneStyleBlockMatches, htmlDocument, styleBlockInfo.start.line); }); } /** * Return all rules matching the specified selector. * For Sprint 4, we only look at the rightmost simple selector. For example, searching for ".foo" will * match these rules: * .foo {} * div .foo {} * div.foo {} * div .foo[bar="42"] {} * div .foo:hovered {} * div .foo::first-child * but will *not* match these rules: * .foobar {} * .foo .bar {} * div .foo .bar {} * .foo.bar {} * * @param {!String} selector The selector to match. This can be a tag selector, class selector or id selector * @param {?Document} htmlDocument An HTML file for context (so we can search <style> blocks) * @return {$.Promise} that will be resolved with an Array of objects containing the * source document, start line, and end line (0-based, inclusive range) for each matching declaration list. * Does not addRef() the documents returned in the array. */ function findMatchingRules(selector, htmlDocument) { var result = new $.Deferred(), resultSelectors = []; // Synchronously search for matches in <style> blocks if (htmlDocument) { _findMatchingRulesInStyleBlocks(htmlDocument, selector, resultSelectors); } // Asynchronously search for matches in all the project's CSS files // (results are appended together in same 'resultSelectors' array) _findMatchingRulesInCSSFiles(selector, resultSelectors) .done(function () { result.resolve(resultSelectors); }) .fail(function (error) { result.reject(error); }); return result.promise(); } exports._findAllMatchingSelectorsInText = _findAllMatchingSelectorsInText; // For testing only exports.findMatchingRules = findMatchingRules; exports.extractAllSelectors = extractAllSelectors; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ // FUTURE: Merge part (or all) of this class with InlineTextEditor /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ /** * An inline editor for displaying and editing multiple text ranges. Each range corresponds to a * contiguous set of lines in a file. * * In the current implementation, only one range is visible at a time. A list on the right side * of the editor allows the user to select which range is visible. * * This module does not dispatch any events. */ define('editor/MultiRangeInlineEditor',['require','exports','module','document/TextRange','editor/InlineTextEditor','editor/EditorManager','command/Commands','strings','command/CommandManager','utils/PerfUtils'],function (require, exports, module) { // Load dependent modules var TextRange = require("document/TextRange").TextRange, InlineTextEditor = require("editor/InlineTextEditor").InlineTextEditor, EditorManager = require("editor/EditorManager"), Commands = require("command/Commands"), Strings = require("strings"), CommandManager = require("command/CommandManager"), PerfUtils = require("utils/PerfUtils"); /** * Remove trailing "px" from a style size value. * @param {!JQuery} $target Element in DOM * @param {!string} styleName Style name to query * @return {number} Style value converted from string to number, removing "px" units */ function _parseStyleSize($target, styleName) { return parseInt($target.css(styleName), 10); } /** * @constructor * Stores one search result: its source file, line range, etc. plus the DOM node representing it * in the results list. */ function SearchResultItem(rangeResult) { this.name = rangeResult.name; this.textRange = new TextRange(rangeResult.document, rangeResult.lineStart, rangeResult.lineEnd); // this.$listItem is assigned in load() } SearchResultItem.prototype.name = null; SearchResultItem.prototype.textRange = null; SearchResultItem.prototype.$listItem = null; function _updateRangeLabel(listItem, range) { var text = range.name + " " + range.textRange.document.file.name + " : " + (range.textRange.startLine + 1); listItem.text(text); listItem.attr("title", text); } /** * @constructor * @param {Array.<{name:String,document:Document,startLine:number,endLine:number}>} ranges The text ranges to display. * @extends {InlineTextEditor} */ function MultiRangeInlineEditor(ranges) { InlineTextEditor.call(this); // Store the results to show in the range list. This creates TextRanges bound to the Document, // which will stay up to date automatically (but we must be sure to detach them later) this._ranges = ranges.map(function (rangeResult) { return new SearchResultItem(rangeResult); }); this._selectedRangeIndex = -1; } MultiRangeInlineEditor.prototype = new InlineTextEditor(); MultiRangeInlineEditor.prototype.constructor = MultiRangeInlineEditor; MultiRangeInlineEditor.prototype.parentClass = InlineTextEditor.prototype; MultiRangeInlineEditor.prototype.$editorsDiv = null; MultiRangeInlineEditor.prototype.$relatedContainer = null; MultiRangeInlineEditor.prototype.$selectedMarker = null; /** @type {Array.<SearchResultItem>} */ MultiRangeInlineEditor.prototype._ranges = null; MultiRangeInlineEditor.prototype._selectedRangeIndex = null; /** * @override * @param {!Editor} hostEditor Outer Editor instance that inline editor will sit within. * */ MultiRangeInlineEditor.prototype.load = function (hostEditor) { this.parentClass.load.call(this, hostEditor); // Container to hold all editors var self = this; // Bind event handlers this._updateRelatedContainer = this._updateRelatedContainer.bind(this); this._ensureCursorVisible = this._ensureCursorVisible.bind(this); this._handleChange = this._handleChange.bind(this); this._onClick = this._onClick.bind(this); // Create DOM to hold editors and related list this.$editorsDiv = $(window.document.createElement("div")).addClass("inlineEditorHolder"); // Outer container for border-left and scrolling this.$relatedContainer = $(window.document.createElement("div")).addClass("related-container"); this._relatedContainerInserted = false; this._relatedContainerInsertedHandler = this._relatedContainerInsertedHandler.bind(this); // FIXME (jasonsj): deprecated event http://www.w3.org/TR/DOM-Level-3-Events/ this.$relatedContainer.on("DOMNodeInserted", this._relatedContainerInsertedHandler); // List "selection" highlight this.$selectedMarker = $(window.document.createElement("div")).appendTo(this.$relatedContainer).addClass("selection"); // Inner container var $related = $(window.document.createElement("div")).appendTo(this.$relatedContainer).addClass("related"); // Range list var $rangeList = $(window.document.createElement("ul")).appendTo($related); // create range list & add listeners for range textrange changes var rangeItemText; this._ranges.forEach(function (range, i) { // Create list item UI var $rangeItem = $(window.document.createElement("li")).appendTo($rangeList); _updateRangeLabel($rangeItem, range); $rangeItem.mousedown(function () { self.setSelectedIndex(i); }); self._ranges[i].$listItem = $rangeItem; // Update list item as TextRange changes $(self._ranges[i].textRange).on("change", function () { _updateRangeLabel($rangeItem, range); }); // If TextRange lost sync, react just as we do for an inline Editor's lostContent event: // close the whole inline widget $(self._ranges[i].textRange).on("lostSync", function () { self.close(); }); }); // select the first range self.setSelectedIndex(0); // attach to main container this.$htmlContent.append(this.$editorsDiv).append(this.$relatedContainer); // initialize position based on the main #editor-holder window.setTimeout(this._updateRelatedContainer, 0); // Changes to the host editor should update the relatedContainer // Note: normally it's not kosher to listen to changes on a specific editor, // but in this case we're specifically concerned with changes in the given // editor, not general document changes. $(this.hostEditor).on("change", this._updateRelatedContainer); // Update relatedContainer when this widget's position changes $(this).on("offsetTopChanged", this._updateRelatedContainer); // Listen to the window resize event to reposition the relatedContainer // when the hostEditor's scrollbars visibility changes $(window).on("resize", this._updateRelatedContainer); // Listen for clicks directly on us, so we can set focus back to the editor this.$htmlContent.on("click", this._onClick); }; /** * Specify the range that is shown in the editor. * * @param {!number} index The index of the range to select. */ MultiRangeInlineEditor.prototype.setSelectedIndex = function (index) { var newIndex = Math.min(Math.max(0, index), this._ranges.length - 1); if (newIndex === this._selectedRangeIndex) { return; } // Remove selected class(es) var previousItem = (this._selectedRangeIndex >= 0) ? this._ranges[this._selectedRangeIndex].$listItem : null; if (previousItem) { previousItem.toggleClass("selected", false); } this._selectedRangeIndex = newIndex; var $rangeItem = this._ranges[this._selectedRangeIndex].$listItem; this._ranges[this._selectedRangeIndex].$listItem.toggleClass("selected", true); // Remove previous editors $(this.editors[0]).off("change", this._updateRelatedContainer); this.editors.forEach(function (editor) { editor.destroy(); //release ref on Document }); this.editors = []; this.$editorsDiv.children().remove(); // Add new editor var range = this._getSelectedRange(); this.createInlineEditorFromText(range.textRange.document, range.textRange.startLine, range.textRange.endLine, this.$editorsDiv.get(0)); this.editors[0].focus(); // Changes in size to the inline editor should update the relatedContainer // Note: normally it's not kosher to listen to changes on a specific editor, // but in this case we're specifically concerned with changes in the given // editor, not general document changes. $(this.editors[0]).on("change", this._handleChange); // Cursor activity in the inline editor may cause us to horizontally scroll. $(this.editors[0]).on("cursorActivity", this._ensureCursorVisible); this.editors[0].refresh(); // ensureVisibility is set to false because we don't want to scroll the main editor when the user selects a view this.sizeInlineWidgetToContents(true, false); this._updateRelatedContainer(); // scroll the selection to the rangeItem, use setTimeout to wait for DOM updates var self = this; window.setTimeout(function () { var containerHeight = self.$relatedContainer.height(), itemTop = $rangeItem.position().top, scrollTop = self.$relatedContainer.scrollTop(); self.$selectedMarker.css("top", itemTop); self.$selectedMarker.height($rangeItem.outerHeight()); if (containerHeight <= 0) { return; } var paddingTop = _parseStyleSize($rangeItem.parent(), "paddingTop"); if ((itemTop - paddingTop) < scrollTop) { self.$relatedContainer.scrollTop(itemTop - paddingTop); } else { var itemBottom = itemTop + $rangeItem.height() + _parseStyleSize($rangeItem.parent(), "paddingBottom"); if (itemBottom > (scrollTop + containerHeight)) { self.$relatedContainer.scrollTop(itemBottom - containerHeight); } } }, 0); }; /** * Called any time inline is closed, whether manually (via closeThisInline()) or automatically */ MultiRangeInlineEditor.prototype.onClosed = function () { this.parentClass.onClosed.call(this); // super.onClosed() // remove resize handlers for relatedContainer $(this.hostEditor).off("change", this._updateRelatedContainer); $(this.editors[0]).off("change", this._handleChange); $(this.editors[0]).off("cursorActivity", this._ensureCursorVisible); $(this).off("offsetTopChanged", this._updateRelatedContainer); $(window).off("resize", this._updateRelatedContainer); // de-ref all the Documents in the search results this._ranges.forEach(function (searchResult) { searchResult.textRange.dispose(); }); }; /** * @private * Set _relatedContainerInserted flag once the $relatedContainer is inserted in the DOM. */ MultiRangeInlineEditor.prototype._relatedContainerInsertedHandler = function () { this.$relatedContainer.off("DOMNodeInserted", this._relatedContainerInsertedHandler); this._relatedContainerInserted = true; }; /** * Prevent clicks in the dead areas of the inlineWidget from changing the focus and insertion point in the editor. * This is done by detecting clicks in the inlineWidget that are not inside the editor or the range list and * restoring focus and the insertion point. */ MultiRangeInlineEditor.prototype._onClick = function (event) { var childEditor = this.editors[0], editorRoot = childEditor.getRootElement(), editorPos = $(editorRoot).offset(); function containsClick($parent) { return $parent.find(event.target) > 0 || $parent[0] === event.target; } // Ignore clicks in editor and clicks on filename link if (!containsClick($(editorRoot)) && !containsClick($(".filename", this.$editorsDiv))) { childEditor.focus(); // Only set the cursor if the click isn't in the range list. if (!containsClick(this.$relatedContainer)) { if (event.pageY < editorPos.top) { childEditor.setCursorPos(0, 0); } else if (event.pageY > editorPos.top + $(editorRoot).height()) { var lastLine = childEditor.getLastVisibleLine(); childEditor.setCursorPos(lastLine, childEditor.document.getLine(lastLine).length); } } } }; /** * Set the size, position, and clip rect of the range list. */ MultiRangeInlineEditor.prototype._updateRelatedContainer = function () { var borderThickness = (this.$htmlContent.outerHeight() - this.$htmlContent.innerHeight()) / 2; this.$relatedContainer.css("top", this.$htmlContent.offset().top + borderThickness); this.$relatedContainer.height(this.$htmlContent.height()); // Because we're using position: fixed, we need to explicitly clip the range list if it crosses // out of the top or bottom of the scroller area. var hostScroller = this.hostEditor.getScrollerElement(), rcTop = this.$relatedContainer.offset().top, rcHeight = this.$relatedContainer.outerHeight(), rcBottom = rcTop + rcHeight, scrollerOffset = $(hostScroller).offset(), scrollerTop = scrollerOffset.top, scrollerBottom = scrollerTop + hostScroller.clientHeight, scrollerLeft = scrollerOffset.left, rightOffset = $(window.document.body).outerWidth() - (scrollerLeft + hostScroller.clientWidth); if (rcTop < scrollerTop || rcBottom > scrollerBottom) { this.$relatedContainer.css("clip", "rect(" + Math.max(scrollerTop - rcTop, 0) + "px, auto, " + (rcHeight - Math.max(rcBottom - scrollerBottom, 0)) + "px, auto)"); } else { this.$relatedContainer.css("clip", ""); } // Constrain relatedContainer width to half of the scroller width var relatedContainerWidth = this.$relatedContainer.width(); if (this._relatedContainerInserted) { if (this._relatedContainerDefaultWidth === undefined) { this._relatedContainerDefaultWidth = relatedContainerWidth; } var halfWidth = Math.floor(hostScroller.clientWidth / 2); relatedContainerWidth = Math.min(this._relatedContainerDefaultWidth, halfWidth); this.$relatedContainer.width(relatedContainerWidth); } // Position immediately to the left of the main editor's scrollbar. this.$relatedContainer.css("right", rightOffset + "px"); // Add extra padding to the right edge of the widget to account for the range list. this.$htmlContent.css("padding-right", this.$relatedContainer.outerWidth() + "px"); // Set the minimum width of the widget (which doesn't include the padding) to the width // of CodeMirror's linespace, so that the total width will be at least as large as the // width of the host editor's code plus the padding for the range list. We need to do this // rather than just setting min-width to 100% because adding padding for the range list // actually pushes out the width of the container, so we would end up continuously // growing the overall width. // This is a bit of a hack since it relies on knowing some detail about the innards of CodeMirror. var lineSpace = this.hostEditor._getLineSpaceElement(), minWidth = $(lineSpace).offset().left - this.$htmlContent.offset().left + lineSpace.scrollWidth; this.$htmlContent.css("min-width", minWidth + "px"); }; /** * Based on the position of the cursor in the inline editor, determine whether we need to change the * scroll position of the host editor to ensure that the cursor is visible. */ MultiRangeInlineEditor.prototype._ensureCursorVisible = function () { if ($.contains(this.editors[0].getRootElement(), window.document.activeElement)) { var cursorCoords = this.editors[0]._codeMirror.cursorCoords(), lineSpaceOffset = $(this.editors[0]._getLineSpaceElement()).offset(), rangeListOffset = this.$relatedContainer.offset(); // If we're off the left-hand side, we just want to scroll it into view normally. But // if we're underneath the range list on the right, we want to ask the host editor to // scroll far enough that the current cursor position is visible to the left of the range // list. (Because we always add extra padding for the range list, this is always possible.) if (cursorCoords.x >= rangeListOffset.left) { cursorCoords.x += this.$relatedContainer.outerWidth(); } // Vertically, we want to set the scroll position relative to the overall host editor, not // the lineSpace of the widget itself. Also, we can't use the lineSpace here, because its top // position just corresponds to whatever CodeMirror happens to have rendered at the top. So // we need to figure out our position relative to the top of the virtual scroll area, which is // the top of the actual scroller minus the scroll position. var scrollerTop = $(this.hostEditor.getScrollerElement()).offset().top - this.hostEditor.getScrollPos().y; this.hostEditor._codeMirror.scrollIntoView(cursorCoords.x - lineSpaceOffset.left, cursorCoords.y - scrollerTop, cursorCoords.x - lineSpaceOffset.left, cursorCoords.yBot - scrollerTop); } }; /** * Handle a change event from the editor. Update the related container and make sure the * cursor is visible. */ MultiRangeInlineEditor.prototype._handleChange = function () { this._updateRelatedContainer(); this._ensureCursorVisible(); }; /** * @return {Array.<SearchResultItem>} */ MultiRangeInlineEditor.prototype._getRanges = function () { return this._ranges; }; /** * @return {!SearchResultItem} */ MultiRangeInlineEditor.prototype._getSelectedRange = function () { return this._ranges[this._selectedRangeIndex]; }; /** * Display the next range in the range list */ MultiRangeInlineEditor.prototype._selectNextRange = function () { this.setSelectedIndex(this._selectedRangeIndex + 1); }; /** * Display the previous range in the range list */ MultiRangeInlineEditor.prototype._selectPreviousRange = function () { this.setSelectedIndex(this._selectedRangeIndex - 1); }; /** * Sizes the inline widget height to be the maximum between the range list height and the editor height * @override * @param {boolean} force the editor to resize * @param {boolean} ensureVisibility makes the parent editor scroll to display the inline editor. Default true. */ MultiRangeInlineEditor.prototype.sizeInlineWidgetToContents = function (force, ensureVisibility) { // Size the code mirror editors height to the editor content this.parentClass.sizeInlineWidgetToContents.call(this, force); // Size the widget height to the max between the editor content and the related ranges list var widgetHeight = Math.max(this.$relatedContainer.find(".related").height(), this.$editorsDiv.height()); this.hostEditor.setInlineWidgetHeight(this, widgetHeight, ensureVisibility); // The related ranges container size itself based on htmlContent which is set by setInlineWidgetHeight above. this._updateRelatedContainer(); }; /** * Returns the currently focused MultiRangeInlineEditor. * @returns {MultiRangeInlineEditor} */ function _getFocusedMultiRangeInlineEditor() { var focusedMultiRangeInlineEditor = null, result = EditorManager.getFocusedInlineWidget(); if (result) { var focusedWidget = result.widget; if (focusedWidget && focusedWidget instanceof MultiRangeInlineEditor) { focusedMultiRangeInlineEditor = focusedWidget; } } return focusedMultiRangeInlineEditor; } /** * Previous Range command handler */ function _previousRange() { var focusedMultiRangeInlineEditor = _getFocusedMultiRangeInlineEditor(); if (focusedMultiRangeInlineEditor) { focusedMultiRangeInlineEditor._selectPreviousRange(); } } /** * Next Range command handler */ function _nextRange() { var focusedMultiRangeInlineEditor = _getFocusedMultiRangeInlineEditor(); if (focusedMultiRangeInlineEditor) { focusedMultiRangeInlineEditor._selectNextRange(); } } CommandManager.register(Strings.CMD_QUICK_EDIT_PREV_MATCH, Commands.QUICK_EDIT_PREV_MATCH, _previousRange); CommandManager.register(Strings.CMD_QUICK_EDIT_NEXT_MATCH, Commands.QUICK_EDIT_NEXT_MATCH, _nextRange); exports.MultiRangeInlineEditor = MultiRangeInlineEditor; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ define('editor/CSSInlineEditor',['require','exports','module','language/CSSUtils','editor/EditorManager','language/HTMLUtils','editor/MultiRangeInlineEditor'],function (require, exports, module) { // Load dependent modules var CSSUtils = require("language/CSSUtils"), EditorManager = require("editor/EditorManager"), HTMLUtils = require("language/HTMLUtils"), MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor; /** * Given a position in an HTML editor, returns the relevant selector for the attribute/tag * surrounding that position, or "" if none is found. * @param {!Editor} editor * @private */ function _getSelectorName(editor, pos) { var tagInfo = HTMLUtils.getTagInfo(editor, pos), selectorName = ""; if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME) { // Type selector selectorName = tagInfo.tagName; } else if (tagInfo.position.tokenType === HTMLUtils.ATTR_NAME || tagInfo.position.tokenType === HTMLUtils.ATTR_VALUE) { if (tagInfo.attr.name === "class") { // Class selector. We only look for the class name // that includes the insertion point. For example, if // the attribute is: // class="error-dialog modal hide" // and the insertion point is inside "modal", we want ".modal" var attributeValue = tagInfo.attr.value; var startIndex = attributeValue.substr(0, tagInfo.position.offset).lastIndexOf(" "); var endIndex = attributeValue.indexOf(" ", tagInfo.position.offset); selectorName = "." + attributeValue.substring( startIndex === -1 ? 0 : startIndex + 1, endIndex === -1 ? attributeValue.length : endIndex ); // If the insertion point is surrounded by space, selectorName is "." // Check for that here if (selectorName === ".") { selectorName = ""; } } else if (tagInfo.attr.name === "id") { // ID selector selectorName = "#" + tagInfo.attr.value; } } return selectorName; } /** * This function is registered with EditManager as an inline editor provider. It creates a CSSInlineEditor * when cursor is on an HTML tag name, class attribute, or id attribute, find associated * CSS rules and show (one/all of them) in an inline editor. * * @param {!Editor} editor * @param {!{line:Number, ch:Number}} pos * @return {$.Promise} a promise that will be resolved with an InlineWidget * or null if we're not going to provide anything. */ function htmlToCSSProvider(hostEditor, pos) { // Only provide a CSS editor when cursor is in HTML content if (hostEditor.getModeForSelection() !== "html") { return null; } // Only provide CSS editor if the selection is within a single line var sel = hostEditor.getSelection(false); if (sel.start.line !== sel.end.line) { return null; } // Always use the selection start for determining selector name. The pos // parameter is usually the selection end. var selectorName = _getSelectorName(hostEditor, hostEditor.getSelection(false).start); if (selectorName === "") { return null; } var result = new $.Deferred(); CSSUtils.findMatchingRules(selectorName, hostEditor.document) .done(function (rules) { if (rules && rules.length > 0) { var cssInlineEditor = new MultiRangeInlineEditor(rules); cssInlineEditor.load(hostEditor); result.resolve(cssInlineEditor); } else { // No matching rules were found. result.reject(); } }) .fail(function () { console.log("Error in findMatchingRules()"); result.reject(); }); return result.promise(); } EditorManager.registerInlineEditProvider(htmlToCSSProvider); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ /** * Defines a ChangedDocumentTracker class to monitor changes to files in the current project. */ define('document/ChangedDocumentTracker',['require','exports','module','document/DocumentManager','project/ProjectManager'],function (require, exports, module) { var DocumentManager = require("document/DocumentManager"), ProjectManager = require("project/ProjectManager"); /** * Tracks "change" events on opened Documents. Used to monitor changes * to documents in-memory and update caches. Assumes all documents have * changed when the Brackets window loses and regains focus. Does not * read timestamps of files on disk. Clients may optionally track file * timestamps on disk independently. */ function ChangedDocumentTracker() { var self = this; this._changedPaths = {}; this._windowFocus = true; this._addListener = this._addListener.bind(this); this._removeListener = this._removeListener.bind(this); this._onChange = this._onChange.bind(this); this._onWindowFocus = this._onWindowFocus.bind(this); $(DocumentManager).on("afterDocumentCreate", function (event, doc) { // Only track documents in the current project if (ProjectManager.isWithinProject(doc.file.fullPath)) { self._addListener(doc); } }); $(DocumentManager).on("beforeDocumentDelete", function (event, doc) { // In case a document somehow remains loaded after its project // has been closed, unconditionally attempt to remove the listener. self._removeListener(doc); }); $(window).focus(this._onWindowFocus); } /** * @private * Assumes all files are changed when the window loses and regains focus. */ ChangedDocumentTracker.prototype._addListener = function (doc) { $(doc).on("change", this._onChange); }; /** * @private */ ChangedDocumentTracker.prototype._removeListener = function (doc) { $(doc).off("change", this._onChange); }; /** * @private * Assumes all files are changed when the window loses and regains focus. */ ChangedDocumentTracker.prototype._onWindowFocus = function (event, doc) { this._windowFocus = true; }; /** * @private * Tracks changed documents. */ ChangedDocumentTracker.prototype._onChange = function (event, doc) { // if it was already changed, and the client hasn't reset the tracker, // then leave it changed. this._changedPaths[doc.file.fullPath] = true; }; /** * Empty the set of dirty paths. Begin tracking new dirty documents. */ ChangedDocumentTracker.prototype.reset = function () { this._changedPaths = {}; this._windowFocus = false; }; /** * Check if a file path is dirty. * @param {!string} file path * @return {!boolean} Returns true if the file was dirtied since the last reset. */ ChangedDocumentTracker.prototype.isPathChanged = function (path) { return this._windowFocus || this._changedPaths[path]; }; /** * Get the set of changed paths since the last reset. * @return {Array.<string>} Changed file paths */ ChangedDocumentTracker.prototype.getChangedPaths = function () { return $.makeArray(this._changedPaths); }; module.exports = ChangedDocumentTracker; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, PathUtils, CodeMirror */ /** * Set of utilities for simple parsing of JS text. */ define('language/JSUtils',['require','exports','module','utils/Async','document/DocumentManager','document/ChangedDocumentTracker','file/NativeFileSystem','utils/PerfUtils','utils/StringUtils'],function (require, exports, module) { // Load brackets modules var Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), ChangedDocumentTracker = require("document/ChangedDocumentTracker"), NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PerfUtils = require("utils/PerfUtils"), StringUtils = require("utils/StringUtils"); /** * Tracks dirty documents between invocations of findMatchingFunctions. * @type {ChangedDocumentTracker} */ var _changedDocumentTracker = new ChangedDocumentTracker(); /** * Function matching regular expression. Recognizes the forms: * "function functionName()", "functionName = function()", and * "functionName: function()". * * Note: JavaScript identifier matching is not strictly to spec. This * RegExp matches any sequence of characters that is not whitespace. * @type {RegExp} */ var _functionRegExp = /(function\s+([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*(\([^)]*\)))|(([$_A-Za-z\u007F-\uFFFF][$_A-Za-z0-9\u007F-\uFFFF]*)\s*[:=]\s*function\s*(\([^)]*\)))/g; /** * @private * Return an Array with names and offsets for all functions in the specified text * @param {!string} text Document text * @return {Object.<string, Array.<{offsetStart: number, offsetEnd: number}>} */ function _findAllFunctionsInText(text) { var results = {}, functionName, match; PerfUtils.markStart(PerfUtils.JSUTILS_REGEXP); while ((match = _functionRegExp.exec(text)) !== null) { functionName = (match[2] || match[5]).trim(); if (!Array.isArray(results[functionName])) { results[functionName] = []; } results[functionName].push({offsetStart: match.index}); } PerfUtils.addMeasurement(PerfUtils.JSUTILS_REGEXP); return results; } // Given the start offset of a function definition (before the opening brace), find // the end offset for the function (the closing "}"). Returns the position one past the // close brace. Properly ignores braces inside comments, strings, and regexp literals. function _getFunctionEndOffset(text, offsetStart) { var mode = CodeMirror.getMode({}, "javascript"); var state = CodeMirror.startState(mode), stream, style, token; var curOffset = offsetStart, length = text.length, blockCount = 0, lineStart; var foundStartBrace = false; // Get a stream for the next line, and update curOffset and lineStart to point to the // beginning of that next line. Returns false if we're at the end of the text. function nextLine() { if (stream) { curOffset++; // account for \n if (curOffset >= length) { return false; } } lineStart = curOffset; var lineEnd = text.indexOf("\n", lineStart); if (lineEnd === -1) { lineEnd = length; } stream = new CodeMirror.StringStream(text.slice(curOffset, lineEnd)); return true; } // Get the next token, updating the style and token to refer to the current // token, and updating the curOffset to point to the end of the token (relative // to the start of the original text). function nextToken() { if (curOffset >= length) { return false; } if (stream) { // Set the start of the next token to the current stream position. stream.start = stream.pos; } while (!stream || stream.eol()) { if (!nextLine()) { return false; } } style = mode.token(stream, state); token = stream.current(); curOffset = lineStart + stream.pos; return true; } while (nextToken()) { if (style !== "comment" && style !== "regexp" && style !== "string") { if (token === "{") { foundStartBrace = true; blockCount++; } else if (token === "}") { blockCount--; } } // blockCount starts at 0, so we don't want to check if it hits 0 // again until we've actually gone past the start of the function body. if (foundStartBrace && blockCount <= 0) { return curOffset; } } // Shouldn't get here, but if we do, return the end of the text as the offset. return length; } /** * @private * Computes function offsetEnd, lineStart and lineEnd. Appends a result record to rangeResults. * @param {!Document} doc * @param {!string} functionName * @param {!Array.<{offsetStart: number, offsetEnd: number}>} functions * @param {!Array.<{document: Document, name: string, lineStart: number, lineEnd: number}>} rangeResults */ function _computeOffsets(doc, functionName, functions, rangeResults) { var text = doc.getText(), lines = StringUtils.getLines(text); functions.forEach(function (funcEntry) { if (!funcEntry.offsetEnd) { PerfUtils.markStart(PerfUtils.JSUTILS_END_OFFSET); funcEntry.offsetEnd = _getFunctionEndOffset(text, funcEntry.offsetStart); funcEntry.lineStart = StringUtils.offsetToLineNum(lines, funcEntry.offsetStart); funcEntry.lineEnd = StringUtils.offsetToLineNum(lines, funcEntry.offsetEnd); PerfUtils.addMeasurement(PerfUtils.JSUTILS_END_OFFSET); } rangeResults.push({ document: doc, name: functionName, lineStart: funcEntry.lineStart, lineEnd: funcEntry.lineEnd }); }); } /** * @private * Read a file and build a function list. Result is cached in fileInfo. * @param {!FileInfo} fileInfo File to parse * @param {!$.Deferred} result Deferred to resolve with all functions found and the document */ function _readFile(fileInfo, result) { DocumentManager.getDocumentForPath(fileInfo.fullPath) .done(function (doc) { var allFunctions = _findAllFunctionsInText(doc.getText()); // Cache the result in the fileInfo object fileInfo.JSUtils = {}; fileInfo.JSUtils.functions = allFunctions; fileInfo.JSUtils.timestamp = doc.diskTimestamp; result.resolve({doc: doc, functions: allFunctions}); }) .fail(function (error) { result.reject(error); }); } /** * Determines if the document function cache is up to date. * @param {FileInfo} fileInfo * @return {$.Promise} A promise resolved with true with true when a function cache is available for the document. Resolves * with false when there is no cache or the cache is stale. */ function _shouldGetFromCache(fileInfo) { var result = new $.Deferred(), isChanged = _changedDocumentTracker.isPathChanged(fileInfo.fullPath); if (isChanged && fileInfo.JSUtils) { // See if it's dirty and in the working set first var doc = DocumentManager.getOpenDocumentForPath(fileInfo.fullPath); if (doc && doc.isDirty) { result.resolve(false); } else { // If a cache exists, check the timestamp on disk var file = new NativeFileSystem.FileEntry(fileInfo.fullPath); file.getMetadata( function (metadata) { result.resolve(fileInfo.JSUtils.timestamp === metadata.diskTimestamp); }, function (error) { result.reject(error); } ); } } else { // Use the cache if the file did not change and the cache exists result.resolve(!isChanged && fileInfo.JSUtils); } return result.promise(); } /** * @private * Compute lineStart and lineEnd for each matched function * @param {!Array.<{doc: Document, fileInfo: FileInfo, functions: Array.<offsetStart: number, offsetEnd: number>}>} docEntries * @param {!string} functionName * @param {!Array.<document: Document, name: string, lineStart: number, lineEnd: number>} rangeResults * @return {$.Promise} A promise resolved with an array of document ranges to populate a MultiRangeInlineEditor. */ function _getOffsetsForFunction(docEntries, functionName) { // Filter for documents that contain the named function var result = new $.Deferred(), matchedDocuments = [], rangeResults = [], functionsInDocument; docEntries.forEach(function (docEntry) { functionsInDocument = docEntry.functions[functionName]; if (functionsInDocument) { matchedDocuments.push({doc: docEntry.doc, fileInfo: docEntry.fileInfo, functions: functionsInDocument}); } }); Async.doInParallel(matchedDocuments, function (docEntry) { var doc = docEntry.doc, oneResult = new $.Deferred(); // doc will be undefined if we hit the cache if (!doc) { DocumentManager.getDocumentForPath(docEntry.fileInfo.fullPath) .done(function (fetchedDoc) { _computeOffsets(fetchedDoc, functionName, docEntry.functions, rangeResults); }) .always(function () { oneResult.resolve(); }); } else { _computeOffsets(doc, functionName, docEntry.functions, rangeResults); oneResult.resolve(); } return oneResult.promise(); }).done(function () { result.resolve(rangeResults); }); return result.promise(); } /** * Resolves with a record containing the Document or FileInfo and an Array of all * function names with offsets for the specified file. Results may be cached. * @param {FileInfo} fileInfo * @return {$.Promise} A promise resolved with a document info object that * contains a map of all function names from the document and each function's start offset. */ function _getFunctionsForFile(fileInfo) { var result = new $.Deferred(); _shouldGetFromCache(fileInfo) .done(function (useCache) { if (useCache) { // Return cached data. doc property is undefined since we hit the cache. // _getOffsets() will fetch the Document if necessary. result.resolve({/*doc: undefined,*/fileInfo: fileInfo, functions: fileInfo.JSUtils.functions}); } else { _readFile(fileInfo, result); } }).fail(function (err) { result.reject(err); }); return result.promise(); } /** * @private * Get all functions for each FileInfo. * @param {Array.<FileInfo>} fileInfos * @return {$.Promise} A promise resolved with an array of document info objects that each * contain a map of all function names from the document and each function's start offset. */ function _getFunctionsInFiles(fileInfos) { var result = new $.Deferred(), docEntries = []; PerfUtils.markStart(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS); Async.doInParallel(fileInfos, function (fileInfo) { var oneResult = new $.Deferred(); _getFunctionsForFile(fileInfo) .done(function (docInfo) { docEntries.push(docInfo); }) .always(function (error) { // If one file fails, continue to search oneResult.resolve(); }); return oneResult.promise(); }).always(function () { // Reset ChangedDocumentTracker now that the cache is up to date. _changedDocumentTracker.reset(); PerfUtils.addMeasurement(PerfUtils.JSUTILS_GET_ALL_FUNCTIONS); result.resolve(docEntries); }); return result.promise(); } /** * Return all functions that have the specified name. * * @param {!String} functionName The name to match. * @param {!Array.<FileIndexManager.FileInfo>} fileInfos The array of files to search. * @return {$.Promise} that will be resolved with an Array of objects containing the * source document, start line, and end line (0-based, inclusive range) for each matching function list. * Does not addRef() the documents returned in the array. */ function findMatchingFunctions(functionName, fileInfos) { var result = new $.Deferred(), jsFiles = [], docEntries = []; // Filter fileInfos for .js files jsFiles = fileInfos.filter(function (fileInfo) { return (/^\.js/i).test(PathUtils.filenameExtension(fileInfo.fullPath)); }); // RegExp search (or cache lookup) for all functions in the project _getFunctionsInFiles(jsFiles).done(function (docEntries) { // Compute offsets for all matched functions _getOffsetsForFunction(docEntries, functionName).done(function (rangeResults) { result.resolve(rangeResults); }); }); return result.promise(); } /** * Finds all instances of the specified functionName in "text". * Returns an Array of Objects with start and end properties. * * @param text {!String} JS text to search * @param functionName {!String} function name to search for * @return {Array.<{offset:number, functionName:string}>} * Array of objects containing the start offset for each matched function name. */ function findAllMatchingFunctionsInText(text, functionName) { var allFunctions = _findAllFunctionsInText(text); var result = []; var lines = text.split("\n"); $.each(allFunctions, function (index, functions) { if (index === functionName || functionName === "*") { functions.forEach(function (funcEntry) { var endOffset = _getFunctionEndOffset(text, funcEntry.offsetStart); result.push({ name: index, lineStart: StringUtils.offsetToLineNum(lines, funcEntry.offsetStart), lineEnd: StringUtils.offsetToLineNum(lines, endOffset) }); }); } }); return result; } PerfUtils.createPerfMeasurement("JSUTILS_GET_ALL_FUNCTIONS", "Parallel file search across project"); PerfUtils.createPerfMeasurement("JSUTILS_REGEXP", "RegExp search for all functions"); PerfUtils.createPerfMeasurement("JSUTILS_END_OFFSET", "Find end offset for a single matched function"); exports.findAllMatchingFunctionsInText = findAllMatchingFunctionsInText; exports._getFunctionEndOffset = _getFunctionEndOffset; // For testing only exports.findMatchingFunctions = findMatchingFunctions; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * WorkingSetView generates the UI for the list of the files user is editing based on the model provided by EditorManager. * The UI allows the user to see what files are open/dirty and allows them to close files and specify the current editor. * */ define('project/WorkingSetView',['require','exports','module','document/DocumentManager','command/CommandManager','command/Commands','editor/EditorManager','project/FileViewController','file/NativeFileSystem','utils/ViewUtils'],function (require, exports, module) { // Load dependent modules var DocumentManager = require("document/DocumentManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), EditorManager = require("editor/EditorManager"), FileViewController = require("project/FileViewController"), NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ViewUtils = require("utils/ViewUtils"); /** Each list item in the working set stores a references to the related document in the list item's data. * Use listItem.data(_FILE_KEY) to get the document reference */ var _FILE_KEY = "file", $openFilesContainer, $openFilesList; /** * @private * Redraw selection when list size changes or DocumentManager currentDocument changes. */ function _fireSelectionChanged() { // redraw selection $openFilesList.trigger("selectionChanged"); // in-lieu of resize events, manually trigger contentChanged to update scroll shadows $openFilesContainer.triggerHandler("contentChanged"); } /** * @private * adds the style 'vertical-scroll' if a vertical scroll bar is present */ function _adjustForScrollbars() { if ($openFilesContainer[0].scrollHeight > $openFilesContainer[0].clientHeight) { if (!$openFilesContainer.hasClass("vertical-scroll")) { $openFilesContainer.addClass("vertical-scroll"); } } else { $openFilesContainer.removeClass("vertical-scroll"); } } /** * @private * Shows/Hides open files list based on working set content. */ function _redraw() { if (DocumentManager.getWorkingSet().length === 0) { $openFilesContainer.hide(); } else { $openFilesContainer.show(); } _adjustForScrollbars(); _fireSelectionChanged(); } /** * Updates the appearance of the list element based on the parameters provided * @private * @param {!HTMLLIElement} listElement * @param {bool} isDirty * @param {bool} canClose */ function _updateFileStatusIcon(listElement, isDirty, canClose) { var $fileStatusIcon = listElement.find(".file-status-icon"); var showIcon = isDirty || canClose; // remove icon if its not needed if (!showIcon && $fileStatusIcon.length !== 0) { $fileStatusIcon.remove(); $fileStatusIcon = null; // create icon if its needed and doesn't exist } else if (showIcon && $fileStatusIcon.length === 0) { $fileStatusIcon = $("<div class='file-status-icon'></div>") .prependTo(listElement) .mousedown(function (e) { // stopPropagation of mousedown for fileStatusIcon so the parent <LI> item, which // selects documents on mousedown, doesn't select the document in the case // when the click is on fileStatusIcon e.stopPropagation(); }) .click(function () { // Clicking the "X" button is equivalent to File > Close; it doesn't merely // remove a file from the working set var file = listElement.data(_FILE_KEY); CommandManager.execute(Commands.FILE_CLOSE, {file: file}); }); } // Set icon's class if ($fileStatusIcon) { // cast to Boolean needed because toggleClass() distinguishes true/false from truthy/falsy $fileStatusIcon.toggleClass("dirty", Boolean(isDirty)); $fileStatusIcon.toggleClass("can-close", Boolean(canClose)); } } /** * Updates the appearance of the list element based on the parameters provided. * @private * @param {!HTMLLIElement} listElement * @param {?Document} selectedDoc */ function _updateListItemSelection(listItem, selectedDoc) { var shouldBeSelected = (selectedDoc && $(listItem).data(_FILE_KEY).fullPath === selectedDoc.file.fullPath); // cast to Boolean needed because toggleClass() distinguishes true/false from truthy/falsy $(listItem).toggleClass("selected", Boolean(shouldBeSelected)); } function isOpenAndDirty(file) { var docIfOpen = DocumentManager.getOpenDocumentForPath(file.fullPath); return (docIfOpen && docIfOpen.isDirty); } /** * Builds the UI for a new list item and inserts in into the end of the list * @private * @param {FileEntry} file * @return {HTMLLIElement} newListItem */ function _createNewListItem(file) { var curDoc = DocumentManager.getCurrentDocument(); // Create new list item with a link var $link = $("<a href='#'></a>").text(file.name); var $newItem = $("<li></li>") .append($link) .data(_FILE_KEY, file); $openFilesContainer.find("ul").append($newItem); // working set item might never have been opened; if so, then it's definitely not dirty // Update the listItem's apperance _updateFileStatusIcon($newItem, isOpenAndDirty(file), false); _updateListItemSelection($newItem, curDoc); $newItem.mousedown(function (e) { FileViewController.openAndSelectDocument(file.fullPath, FileViewController.WORKING_SET_VIEW); e.preventDefault(); }); $newItem.hover( function () { _updateFileStatusIcon($(this), isOpenAndDirty(file), true); }, function () { _updateFileStatusIcon($(this), isOpenAndDirty(file), false); } ); } /** * Deletes all the list items in the view and rebuilds them from the working set model * @private */ function _rebuildWorkingSet() { $openFilesContainer.find("ul").empty(); DocumentManager.getWorkingSet().forEach(function (file) { _createNewListItem(file); }); _redraw(); } /** * Finds the listItem item assocated with the file. Returns null if not found. * @private * @param {!FileEntry} file * @return {HTMLLIItem} */ function _findListItemFromFile(file) { var result = null; if (file) { var items = $openFilesContainer.find("ul").children(); items.each(function () { var $listItem = $(this); if ($listItem.data(_FILE_KEY).fullPath === file.fullPath) { result = $listItem; return false; // breaks each } }); } return result; } /** * @private */ function _scrollSelectedDocIntoView() { if (FileViewController.getFileSelectionFocus() !== FileViewController.WORKING_SET_VIEW) { return; } var doc = DocumentManager.getCurrentDocument(); if (!doc) { return; } var $selectedDoc = _findListItemFromFile(doc.file); if (!$selectedDoc) { return; } ViewUtils.scrollElementIntoView($openFilesContainer, $selectedDoc, false); } /** * @private */ function _updateListSelection() { var doc; if (FileViewController.getFileSelectionFocus() === FileViewController.WORKING_SET_VIEW) { doc = DocumentManager.getCurrentDocument(); } else { doc = null; } // Iterate through working set list and update the selection on each var items = $openFilesContainer.find("ul").children().each(function () { _updateListItemSelection(this, doc); }); // Make sure selection is in view _scrollSelectedDocIntoView(); _fireSelectionChanged(); } /** * @private */ function _handleFileAdded(file) { _createNewListItem(file); _redraw(); } /** * @private */ function _handleFileListAdded(files) { files.forEach(function (file) { _createNewListItem(file); }); _redraw(); } /** * @private */ function _handleDocumentSelectionChange() { _updateListSelection(); _fireSelectionChanged(); } /** * @private * @param {FileEntry} file */ function _handleFileRemoved(file) { var $listItem = _findListItemFromFile(file); if ($listItem) { $listItem.remove(); } _redraw(); } function _handleRemoveList(removedFiles) { removedFiles.forEach(function (file) { var $listItem = _findListItemFromFile(file); if ($listItem) { $listItem.remove(); } }); _redraw(); } /** * @private * @param {Document} doc */ function _handleDirtyFlagChanged(doc) { var listItem = _findListItemFromFile(doc.file); if (listItem) { var canClose = $(listItem).find(".can-close").length === 1; _updateFileStatusIcon(listItem, doc.isDirty, canClose); } } function create(element) { // Init DOM element $openFilesContainer = element; $openFilesList = $openFilesContainer.find("ul"); // Register listeners $(DocumentManager).on("workingSetAdd", function (event, addedFile) { _handleFileAdded(addedFile); }); $(DocumentManager).on("workingSetAddList", function (event, addedFiles) { _handleFileListAdded(addedFiles); }); $(DocumentManager).on("workingSetRemove", function (event, removedFile) { _handleFileRemoved(removedFile); }); $(DocumentManager).on("workingSetRemoveList", function (event, removedFiles) { _handleRemoveList(removedFiles); }); $(DocumentManager).on("dirtyFlagChange", function (event, doc) { _handleDirtyFlagChanged(doc); }); $(FileViewController).on("documentSelectionFocusChange fileViewFocusChange", _handleDocumentSelectionChange); // Show scroller shadows when open-files-container scrolls ViewUtils.addScrollerShadow($openFilesContainer[0], null, true); ViewUtils.sidebarList($openFilesContainer); _redraw(); } exports.create = create; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, PathUtils, window */ define('document/DocumentCommandHandlers',['require','exports','module','thirdparty/path-utils/path-utils.min','command/CommandManager','command/Commands','command/KeyBindingManager','file/NativeFileSystem','project/ProjectManager','document/DocumentManager','editor/EditorManager','project/FileViewController','file/FileUtils','utils/StringUtils','utils/Async','widgets/Dialogs','strings','preferences/PreferencesManager','utils/PerfUtils'],function (require, exports, module) { require("thirdparty/path-utils/path-utils.min"); // Load dependent modules var CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), KeyBindingManager = require("command/KeyBindingManager"), NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), FileViewController = require("project/FileViewController"), FileUtils = require("file/FileUtils"), StringUtils = require("utils/StringUtils"), Async = require("utils/Async"), Dialogs = require("widgets/Dialogs"), Strings = require("strings"), PreferencesManager = require("preferences/PreferencesManager"), PerfUtils = require("utils/PerfUtils"); /** * Handlers for commands related to document handling (opening, saving, etc.) */ /** @type {jQueryObject} Container for label shown above editor; must be an inline element */ var _$title = null; /** @type {jQueryObject} Container for dirty dot; must be an inline element */ var _$dirtydot = null; /** @type {jQueryObject} Container for _$title; need not be an inline element */ var _$titleWrapper = null; /** @type {string} Label shown above editor for current document: filename and potentially some of its path */ var _currentTitlePath = null; /** @type {jQueryObject} Container for _$titleWrapper; if changing title changes this element's height, must kick editor to resize */ var _$titleContainerToolbar = null; /** @type {Number} Last known height of _$titleContainerToolbar */ var _lastToolbarHeight = null; function updateTitle() { var currentDoc = DocumentManager.getCurrentDocument(); if (currentDoc) { _$title.text(_currentTitlePath); _$title.attr("title", currentDoc.file.fullPath); // dirty dot is always in DOM so layout doesn't change, and visibility is toggled _$dirtydot.css("visibility", (currentDoc.isDirty) ? "visible" : "hidden"); } else { _$title.text(""); _$title.attr("title", ""); _$dirtydot.css("visibility", "hidden"); } // Set _$titleWrapper to a fixed width just large enough to accomodate _$title. This seems equivalent to what // the browser would do automatically, but the CSS trick we use for layout requires _$titleWrapper to have a // fixed width set on it (see the "#main-toolbar.toolbar" CSS rule for details). _$titleWrapper.css("width", ""); var newWidth = _$title.width(); _$titleWrapper.css("width", newWidth); // Changing the width of the title may cause the toolbar layout to change height, which needs to resize the // editor beneath it (toolbar changing height due to window resize is already caught by EditorManager). var newToolbarHeight = _$titleContainerToolbar.height(); if (_lastToolbarHeight !== newToolbarHeight) { _lastToolbarHeight = newToolbarHeight; EditorManager.resizeEditor(); } } function handleCurrentDocumentChange() { var newDocument = DocumentManager.getCurrentDocument(); var perfTimerName = PerfUtils.markStart("DocumentCommandHandlers._onCurrentDocumentChange():\t" + (!newDocument || newDocument.file.fullPath)); if (newDocument) { var fullPath = newDocument.file.fullPath; // In the main toolbar, show the project-relative path (if the file is inside the current project) // or the full absolute path (if it's not in the project). _currentTitlePath = ProjectManager.makeProjectRelativeIfPossible(fullPath); } else { _currentTitlePath = null; } // Update title text & "dirty dot" display updateTitle(); PerfUtils.addMeasurement(perfTimerName); } function handleDirtyChange(event, changedDoc) { var currentDoc = DocumentManager.getCurrentDocument(); if (currentDoc && changedDoc.file.fullPath === currentDoc.file.fullPath) { updateTitle(); } } /** * @private * Creates a document and displays an editor for the specified file path. * @param {!string} fullPath * @return {$.Promise} a jQuery promise that will be resolved with a * document for the specified file path, or rejected if the file can not be read. */ function doOpen(fullPath) { var result = new $.Deferred(); if (!fullPath) { console.log("doOpen() called without fullPath"); result.reject(); } else { var perfTimerName = PerfUtils.markStart("Open File:\t" + fullPath); result.always(function () { PerfUtils.addMeasurement(perfTimerName); }); // Load the file if it was never open before, and then switch to it in the UI DocumentManager.getDocumentForPath(fullPath) .done(function (doc) { DocumentManager.setCurrentDocument(doc); result.resolve(doc); }) .fail(function (fileError) { FileUtils.showFileOpenError(fileError.code, fullPath).done(function () { // For performance, we do lazy checking of file existence, so it may be in working set DocumentManager.removeFromWorkingSet(new NativeFileSystem.FileEntry(fullPath)); EditorManager.focusEditor(); result.reject(); }); }); } return result.promise(); } /** * @private * Used to track the default directory for the file open dialog */ var _defaultOpenDialogFullPath = null; /** * @private * Creates a document and displays an editor for the specified file path. * If no path is specified, a file prompt is provided for input. * @param {?string} fullPath - The path of the file to open; if it's null we'll prompt for it * @return {$.Promise} a jQuery promise that will be resolved with a new * document for the specified file path, or rejected if the file can not be read. */ function _doOpenWithOptionalPath(fullPath) { var result; if (!fullPath) { // Create placeholder deferred result = new $.Deferred(); //first time through, default to the current project path if (!_defaultOpenDialogFullPath) { _defaultOpenDialogFullPath = ProjectManager.getProjectRoot().fullPath; } // Prompt the user with a dialog NativeFileSystem.showOpenDialog(true, false, Strings.OPEN_FILE, _defaultOpenDialogFullPath, null, function (paths) { var i; if (paths.length > 0) { // Add all files to the working set without verifying that // they still exist on disk (for faster opening) var filesToOpen = []; paths.forEach(function (file) { filesToOpen.push(new NativeFileSystem.FileEntry(file)); }); DocumentManager.addListToWorkingSet(filesToOpen); doOpen(paths[paths.length - 1]) .done(function (doc) { var url = PathUtils.parseUrl(doc.file.fullPath); //reconstruct the url but use the directory and stop there _defaultOpenDialogFullPath = url.protocol + url.doubleSlash + url.authority + url.directory; DocumentManager.addToWorkingSet(doc.file); }) // Send the resulting document that was opened .pipe(result.resolve, result.reject); } else { // Reject if the user canceled the dialog result.reject(); } }); } else { result = doOpen(fullPath); } return result.promise(); } /** * Opens the given file and makes it the current document. Does NOT add it to the working set. * @param {!{fullPath:string}} Params for FILE_OPEN command */ function handleFileOpen(commandData) { var fullPath = null; if (commandData) { fullPath = commandData.fullPath; } return _doOpenWithOptionalPath(fullPath) .always(EditorManager.focusEditor); } /** * Opens the given file, makes it the current document, AND adds it to the working set. * @param {!{fullPath:string}} Params for FILE_OPEN command */ function handleFileAddToWorkingSet(commandData) { return handleFileOpen(commandData).done(function (doc) { // addToWorkingSet is synchronous DocumentManager.addToWorkingSet(doc.file); }); } /** * @private * Ensures the suggested file name doesn't already exit. * @param {string} dir The directory to use * @param {string} baseFileName The base to start with, "-n" will get appened to make unique * @param {string} fileExt The file extension * @return {$.Promise} a jQuery promise that will be resolved with a unique name starting with * the given base name */ function _getUntitledFileSuggestion(dir, baseFileName, fileExt) { var result = new $.Deferred(); var suggestedName = baseFileName + fileExt; var dirEntry = new NativeFileSystem.DirectoryEntry(dir); result.progress(function attemptNewName(suggestedName, nextIndexToUse) { if (nextIndexToUse > 99) { //we've tried this enough result.reject(); return; } //check this name dirEntry.getFile( suggestedName, {}, function successCallback(entry) { //file exists, notify to the next progress result.notify(baseFileName + "-" + nextIndexToUse + fileExt, nextIndexToUse + 1); }, function errorCallback(error) { //most likely error is FNF, user is better equiped to handle the rest result.resolve(suggestedName); } ); }); //kick it off result.notify(baseFileName + fileExt, 1); return result.promise(); } /** * Prevents re-entrancy into handleFileNewInProject() * * handleFileNewInProject() first prompts the user to name a file and then asynchronously writes the file when the * filename field loses focus. This boolean prevent additional calls to handleFileNewInProject() when an existing * file creation call is outstanding */ var fileNewInProgress = false; function handleFileNewInProject() { if (fileNewInProgress) { ProjectManager.forceFinishRename(); return; } fileNewInProgress = true; // Determine the directory to put the new file // If a file is currently selected, put it next to it. // If a directory is currently selected, put it in it. // If nothing is selected, put it at the root of the project var baseDir, selected = ProjectManager.getSelectedItem() || ProjectManager.getProjectRoot(); baseDir = selected.fullPath; if (selected.isFile) { baseDir = baseDir.substr(0, baseDir.lastIndexOf("/")); } // Create the new node. The createNewItem function does all the heavy work // of validating file name, creating the new file and selecting. var deferred = _getUntitledFileSuggestion(baseDir, Strings.UNTITLED, ".js"); var createWithSuggestedName = function (suggestedName) { ProjectManager.createNewItem(baseDir, suggestedName, false) .pipe(deferred.resolve, deferred.reject, deferred.notify) .always(function () { fileNewInProgress = false; }) .done(function (entry) { FileViewController.addToWorkingSetAndSelect(entry.fullPath, FileViewController.PROJECT_MANAGER); }); }; deferred.done(createWithSuggestedName); deferred.fail(function createWithDefault() { createWithSuggestedName("Untitled.js"); }); return deferred; } function showSaveFileError(code, path) { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_SAVING_FILE_TITLE, StringUtils.format( Strings.ERROR_SAVING_FILE, StringUtils.htmlEscape(path), FileUtils.getFileErrorString(code) ) ); } /** Note: if there is an error, the promise is not rejected until the user has dimissed the dialog */ function doSave(docToSave) { var result = new $.Deferred(); function handleError(error, fileEntry) { showSaveFileError(error.code, fileEntry.fullPath) .always(function () { result.reject(error); }); } if (docToSave && docToSave.isDirty) { var fileEntry = docToSave.file; var writeError = false; fileEntry.createWriter( function (writer) { writer.onwriteend = function () { // Per spec, onwriteend is called after onerror too if (!writeError) { docToSave.notifySaved(); result.resolve(); } }; writer.onerror = function (error) { writeError = true; handleError(error, fileEntry); }; // We don't want normalized line endings, so it's important to pass true to getText() writer.write(docToSave.getText(true)); }, function (error) { handleError(error, fileEntry); } ); } else { result.resolve(); } result.always(function () { EditorManager.focusEditor(); }); return result.promise(); } /** * Saves the given file. If no file specified, assumes the current document. * @param {?{doc: Document}} commandData Document to close, or null * @return {$.Promise} a promise that is resolved after the save completes */ function handleFileSave(commandData) { // Default to current document if doc is null var doc = null; if (commandData) { doc = commandData.doc; } if (!doc) { var focusedEditor = EditorManager.getFocusedEditor(); if (focusedEditor) { doc = focusedEditor.document; } // doc may still be null, e.g. if no editors are open, but doSave() does a null check on // doc and makes sure the document is dirty before saving. } return doSave(doc); } /** * Saves all unsaved documents. Returns a Promise that will be resolved once ALL the save * operations have been completed. If ANY save operation fails, an error dialog is immediately * shown and the other files wait to save until it is dismissed; after all files have been * processed, the Promise is rejected if any ONE save operation failed. * * @return {$.Promise} */ function saveAll() { // Do in serial because doSave shows error UI for each file, and we don't want to stack // multiple dialogs on top of each other return Async.doSequentially( DocumentManager.getWorkingSet(), function (file) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc) { return doSave(doc); } else { // working set entry that was never actually opened - ignore return (new $.Deferred()).resolve().promise(); } }, false ); } /** * Saves all unsaved documents. * @return {$.Promise} a promise that is resolved once ALL the saves have been completed; or rejected * after all operations completed if any ONE of them failed. */ function handleFileSaveAll() { return saveAll(); } /** * Reverts the Document to the current contents of its file on disk. Discards any unsaved changes * in the Document. * @param {Document} doc * @return {$.Promise} a Promise that's resolved when done, or rejected with a FileError if the * file cannot be read (after showing an error dialog to the user). */ function doRevert(doc) { var result = new $.Deferred(); FileUtils.readAsText(doc.file) .done(function (text, readTimestamp) { doc.refreshText(text, readTimestamp); result.resolve(); }) .fail(function (error) { FileUtils.showFileOpenError(error.code, doc.file.fullPath) .always(function () { result.reject(error); }); }); return result.promise(); } /** * Closes the specified file: removes it from the working set, and closes the main editor if one * is open. Prompts user about saving changes first, if document is dirty. * * @param {?{file: FileEntry, promptOnly:boolean}} commandData Optional bag of arguments: * file - File to close; assumes the current document if not specified. * promptOnly - If true, only displays the relevant confirmation UI and does NOT actually * close the document. This is useful when chaining file-close together with other user * prompts that may be cancelable. * @return {$.Promise} a promise that is resolved when the file is closed, or if no file is open. * FUTURE: should we reject the promise if no file is open? */ function handleFileClose(commandData) { // If not specified, file defaults to null; promptOnly defaults to falsy var file = commandData && commandData.file, promptOnly = commandData && commandData.promptOnly; // utility function for handleFileClose: closes document & removes from working set function doClose(file) { if (!promptOnly) { // This selects a different document if the working set has any other options DocumentManager.closeFullEditor(file); EditorManager.focusEditor(); } } var result = new $.Deferred(), promise = result.promise(); // Default to current document if doc is null if (!file) { if (DocumentManager.getCurrentDocument()) { file = DocumentManager.getCurrentDocument().file; } } // No-op if called when nothing is open; TODO: (issue #273) should command be grayed out instead? if (!file) { result.resolve(); return promise; } var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc && doc.isDirty) { // Document is dirty: prompt to save changes before closing var filename = PathUtils.parseUrl(doc.file.fullPath).filename; Dialogs.showModalDialog( Dialogs.DIALOG_ID_SAVE_CLOSE, Strings.SAVE_CLOSE_TITLE, StringUtils.format(Strings.SAVE_CLOSE_MESSAGE, StringUtils.htmlEscape(filename)) ).done(function (id) { if (id === Dialogs.DIALOG_BTN_CANCEL) { result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // "Save" case: wait until we confirm save has succeeded before closing doSave(doc) .done(function () { doClose(file); result.resolve(); }) .fail(function () { result.reject(); }); } else { // "Don't Save" case: even though we're closing the main editor, other views of // the Document may remain in the UI. So we need to revert the Document to a clean // copy of whatever's on disk. doClose(file); // Only reload from disk if we've executed the Close for real, // *and* if at least one other view still exists if (!promptOnly && DocumentManager.getOpenDocumentForPath(file.fullPath)) { doRevert(doc) .pipe(result.resolve, result.reject); } else { result.resolve(); } } }); result.always(function () { EditorManager.focusEditor(); }); } else { // File is not open, or IS open but Document not dirty: close immediately doClose(file); EditorManager.focusEditor(); result.resolve(); } return promise; } /** * Closes all open documents; equivalent to calling handleFileClose() for each document, except * that unsaved changes are confirmed once, in bulk. * @param {?{promptOnly: boolean}} If true, only displays the relevant confirmation UI and does NOT * actually close any documents. This is useful when chaining close-all together with * other user prompts that may be cancelable. * @return {$.Promise} a promise that is resolved when all files are closed */ function handleFileCloseAll(commandData) { var result = new $.Deferred(); var unsavedDocs = []; DocumentManager.getWorkingSet().forEach(function (file) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc && doc.isDirty) { unsavedDocs.push(doc); } }); if (unsavedDocs.length === 0) { // No unsaved changes, so we can proceed without a prompt result.resolve(); } else if (unsavedDocs.length === 1) { // Only one unsaved file: show the usual single-file-close confirmation UI var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: commandData.promptOnly }; handleFileClose(fileCloseArgs).done(function () { // still need to close any other, non-unsaved documents result.resolve(); }).fail(function () { result.reject(); }); } else { // Multiple unsaved files: show a single bulk prompt listing all files var message = Strings.SAVE_CLOSE_MULTI_MESSAGE; message += "<ul>"; unsavedDocs.forEach(function (doc) { message += "<li><span class='dialog-filename'>" + StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)) + "</span></li>"; }); message += "</ul>"; Dialogs.showModalDialog( Dialogs.DIALOG_ID_SAVE_CLOSE, Strings.SAVE_CLOSE_TITLE, message ).done(function (id) { if (id === Dialogs.DIALOG_BTN_CANCEL) { result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // Save all unsaved files, then if that succeeds, close all saveAll().done(function () { result.resolve(); }).fail(function () { result.reject(); }); } else { // "Don't Save" case--we can just go ahead and close all files. result.resolve(); } }); } // If all the unsaved-changes confirmations pan out above, then go ahead & close all editors // NOTE: this still happens before any done() handlers added by our caller, because jQ // guarantees that handlers run in the order they are added. result.done(function () { if (!commandData || !commandData.promptOnly) { DocumentManager.closeAll(); } }); return result.promise(); } /** * @private - tracks our closing state if we get called again */ var _windowGoingAway = false; /** * @private * Common implementation for close/quit/reload which all mostly * the same except for the final step */ function _handleWindowGoingAway(commandData, postCloseHandler, failHandler) { if (_windowGoingAway) { //if we get called back while we're closing, then just return return (new $.Deferred()).resolve().promise(); } //prevent the default action of closing the window until we can save all the files if (commandData && commandData.evt && commandData.evt.cancelable) { commandData.evt.preventDefault(); } return CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true }) .done(function () { _windowGoingAway = true; PreferencesManager.savePreferences(); postCloseHandler(); }) .fail(function () { _windowGoingAway = false; if (failHandler) { failHandler(); } }); } /** * @private * Implementation for abortQuit callback to reset quit sequence settings */ function _handleAbortQuit() { _windowGoingAway = false; } /** Confirms any unsaved changes, then closes the window */ function handleFileCloseWindow(commandData) { return _handleWindowGoingAway( commandData, function () { window.close(); }, function () { // if fail, tell the app to abort any pending quit operation. // TODO: remove this if statement when we move to the new CEF3 shell if (brackets.app.abortQuit) { brackets.app.abortQuit(); } } ); } /** Closes the window, then quits the app */ function handleFileQuit(commandData) { return _handleWindowGoingAway( commandData, function () { brackets.app.quit(); }, function () { // if fail, don't exit: user canceled (or asked us to save changes first, but we failed to do so) // TODO: remove this if statement when we move to the new CEF3 shell if (brackets.app.abortQuit) { brackets.app.abortQuit(); } } ); } /** Does a full reload of the browser window */ function handleFileReload(commandData) { return _handleWindowGoingAway(commandData, function () { window.location.reload(true); }); } /** Are we already listening for a keyup to call detectDocumentNavEnd()? */ var _addedNavKeyHandler = false; /** * When the Ctrl key is released, if we were in the middle of a next/prev document navigation * sequence, now is the time to end it and update the MRU order. If we allowed the order to update * on every next/prev increment, the 1st & 2nd entries would just switch places forever and we'd * never get further down the list. * @param {jQueryEvent} event Key-up event */ function detectDocumentNavEnd(event) { if (event.keyCode === 17) { // Ctrl key DocumentManager.finalizeDocumentNavigation(); _addedNavKeyHandler = false; $(window.document.body).off("keyup", detectDocumentNavEnd); } } /** Navigate to the next/previous (MRU) document. Don't update MRU order yet */ function goNextPrevDoc(inc) { var file = DocumentManager.getNextPrevFile(inc); if (file) { DocumentManager.beginDocumentNavigation(); CommandManager.execute(Commands.FILE_OPEN, { fullPath: file.fullPath }); // Listen for ending of Ctrl+Tab sequence if (!_addedNavKeyHandler) { _addedNavKeyHandler = true; $(window.document.body).keyup(detectDocumentNavEnd); } } } function handleGoNextDoc() { goNextPrevDoc(+1); } function handleGoPrevDoc() { goNextPrevDoc(-1); } function init($titleContainerToolbar) { _$titleContainerToolbar = $titleContainerToolbar; _$titleWrapper = $(".title-wrapper", _$titleContainerToolbar); _$title = $(".title", _$titleWrapper); _$dirtydot = $(".dirty-dot", _$titleWrapper); // Register global commands CommandManager.register(Strings.CMD_FILE_OPEN, Commands.FILE_OPEN, handleFileOpen); CommandManager.register(Strings.CMD_ADD_TO_WORKING_SET, Commands.FILE_ADD_TO_WORKING_SET, handleFileAddToWorkingSet); // TODO: (issue #274) For now, hook up File > New to the "new in project" handler. Eventually // File > New should open a new blank tab, and handleFileNewInProject should // be called from a "+" button in the project CommandManager.register(Strings.CMD_FILE_NEW, Commands.FILE_NEW, handleFileNewInProject); CommandManager.register(Strings.CMD_FILE_SAVE, Commands.FILE_SAVE, handleFileSave); CommandManager.register(Strings.CMD_FILE_SAVE_ALL, Commands.FILE_SAVE_ALL, handleFileSaveAll); CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose); CommandManager.register(Strings.CMD_FILE_CLOSE_ALL, Commands.FILE_CLOSE_ALL, handleFileCloseAll); CommandManager.register(Strings.CMD_CLOSE_WINDOW, Commands.FILE_CLOSE_WINDOW, handleFileCloseWindow); CommandManager.register(Strings.CMD_QUIT, Commands.FILE_QUIT, handleFileQuit); CommandManager.register(Strings.CMD_REFRESH_WINDOW, Commands.DEBUG_REFRESH_WINDOW, handleFileReload); CommandManager.register(Strings.CMD_NEXT_DOC, Commands.NAVIGATE_NEXT_DOC, handleGoNextDoc); CommandManager.register(Strings.CMD_PREV_DOC, Commands.NAVIGATE_PREV_DOC, handleGoPrevDoc); CommandManager.register(Strings.CMD_ABORT_QUIT, Commands.APP_ABORT_QUIT, _handleAbortQuit); KeyBindingManager.addBinding(Commands.NAVIGATE_NEXT_DOC, [{key: "Ctrl-Tab", platform: "win"}, {key: "Ctrl-Tab", platform: "mac"}]); KeyBindingManager.addBinding(Commands.NAVIGATE_PREV_DOC, [{key: "Ctrl-Shift-Tab", platform: "win"}, {key: "Ctrl-Shift-Tab", platform: "mac"}]); // Listen for changes that require updating the editor titlebar $(DocumentManager).on("dirtyFlagChange", handleDirtyChange); $(DocumentManager).on("currentDocumentChange", handleCurrentDocumentChange); } // Define public API exports.init = init; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, FileError */ /** * FileSyncManager is a set of utilities to help track external modifications to the files and folders * in the currently open project. * * Currently, we look for external changes purely by checking file timestamps against the last-sync * timestamp recorded on Document. Later, we will use actual native directory-watching callbacks * instead. * * FUTURE: Whenever we have a 'project file tree model,' we should manipulate that instead of notifying * DocumentManager directly. DocumentManager, the tree UI, etc. then all listen to that model for changes. */ define('project/FileSyncManager',['require','exports','module','project/ProjectManager','document/DocumentManager','editor/EditorManager','command/Commands','command/CommandManager','utils/Async','widgets/Dialogs','strings','utils/StringUtils','file/FileUtils'],function (require, exports, module) { // Load dependent modules var ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), Async = require("utils/Async"), Dialogs = require("widgets/Dialogs"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), FileUtils = require("file/FileUtils"); /** * Guard to spot re-entrancy while syncOpenDocuments() is still in progress * @type {boolean} */ var _alreadyChecking = false; /** * If true, we should bail from the syncOpenDocuments() process and then re-run it. See * comments in syncOpenDocuments() for how this works. * @type {boolean} */ var _restartPending = false; /** @type {Array.<Document>} */ var toReload; /** @type {Array.<Document>} */ var toClose; /** @type {Array.<Document>} */ var editConflicts; /** @type {Array.<Document>} */ var deleteConflicts; /** * Scans all the given Documents for changes on disk, and sorts them into four buckets, * populating the corresponding arrays: * toReload - changed on disk; unchanged within Brackets * toClose - deleted on disk; unchanged within Brackets * editConflicts - changed on disk; also dirty in Brackets * deleteConflicts - deleted on disk; also dirty in Brackets * * @param {!Array.<Document>} docs * @return {$.Promise} Resolved when all scanning done, or rejected immediately if there's any * error while reading file timestamps. Errors are logged but no UI is shown. */ function findExternalChanges(docs) { toReload = []; toClose = []; editConflicts = []; deleteConflicts = []; function checkDoc(doc) { var result = new $.Deferred(); // Check file timestamp / existence doc.file.getMetadata( function (metadata) { // Does file's timestamp differ from last sync time on the Document? if (metadata.modificationTime.getTime() !== doc.diskTimestamp.getTime()) { if (doc.isDirty) { editConflicts.push(doc); } else { toReload.push(doc); } } result.resolve(); }, function (error) { // File has been deleted externally if (error.code === FileError.NOT_FOUND_ERR) { if (doc.isDirty) { deleteConflicts.push(doc); } else { toClose.push(doc); } result.resolve(); } else { // Some other error fetching metadata: treat as a real error console.log("Error checking modification status of " + doc.file.fullPath, error.code); result.reject(); } } ); return result.promise(); } // Check all docs in parallel // (fail fast b/c we won't continue syncing if there was any error fetching timestamps) return Async.doInParallel(docs, checkDoc, true); } /** * Scans all the files in the working set that do not have Documents (and thus were not scanned * by findExternalChanges()). If any were deleted on disk, removes them from the working set. */ function syncUnopenWorkingSet() { // We only care about working set entries that have never been open (have no Document). var unopenWorkingSetFiles = DocumentManager.getWorkingSet().filter(function (wsFile) { return !DocumentManager.getOpenDocumentForPath(wsFile.fullPath); }); function checkWorkingSetFile(file) { var result = new $.Deferred(); file.getMetadata( function (metadata) { // File still exists result.resolve(); }, function (error) { // File has been deleted externally if (error.code === FileError.NOT_FOUND_ERR) { DocumentManager.notifyFileDeleted(file); result.resolve(); } else { // Some other error fetching metadata: treat as a real error console.log("Error checking for deletion of " + file.fullPath, error.code); result.reject(); } } ); return result.promise(); } // Check all these files in parallel return Async.doInParallel(unopenWorkingSetFiles, checkWorkingSetFile, false); } /** * Reloads the Document's contents from disk, discarding any unsaved changes in the editor. * * @param {!Document} doc * @return {$.Promise} Resolved after editor has been refreshed; rejected if unable to load the * file's new content. Errors are logged but no UI is shown. */ function reloadDoc(doc) { var promise = FileUtils.readAsText(doc.file); promise.done(function (text, readTimestamp) { doc.refreshText(text, readTimestamp); }); promise.fail(function (error) { console.log("Error reloading contents of " + doc.file.fullPath, error.code); }); return promise; } /** * Reloads all the documents in "toReload" silently (no prompts). The operations are all run * in parallel. * @return {$.Promise} Resolved/rejected after all reloads done; will be rejected if any one * file's reload failed. Errors are logged (by reloadDoc()) but no UI is shown. */ function reloadChangedDocs() { // Reload each doc in turn, and once all are (async) done, signal that we're done return Async.doInParallel(toReload, reloadDoc, false); } /** * @param {FileError} error * @param {!Document} doc * @return {$.Promise} */ function showReloadError(error, doc) { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_RELOADING_FILE_TITLE, StringUtils.format( Strings.ERROR_RELOADING_FILE, StringUtils.htmlEscape(doc.file.fullPath), FileUtils.getFileErrorString(error.code) ) ); } /** * Closes all the documents in "toClose" silently (no prompts). Completes synchronously. */ function closeDeletedDocs() { toClose.forEach(function (doc) { DocumentManager.notifyFileDeleted(doc.file); }); } /** * Walks through all the documents in "editConflicts" & "deleteConflicts" and prompts the user * about each one. Processing is sequential: if the user chooses to reload a document, the next * prompt is not shown until after the reload has completed. * * @return {$.Promise} Resolved/rejected after all documents have been prompted and (if * applicable) reloaded (and any resulting error UI has been dismissed). Rejected if any * one reload failed. */ function presentConflicts() { var allConflicts = editConflicts.concat(deleteConflicts); function presentConflict(doc, i) { var result = new $.Deferred(), promise = result.promise(); // If window has been re-focused, skip all remaining conflicts so the sync can bail & restart if (_restartPending) { result.resolve(); return promise; } var message; var dialogId; var toClose; // Prompt UI varies depending on whether the file on disk was modified vs. deleted if (i < editConflicts.length) { toClose = false; dialogId = Dialogs.DIALOG_ID_EXT_CHANGED; message = StringUtils.format( Strings.EXT_MODIFIED_MESSAGE, StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)) ); } else { toClose = true; dialogId = Dialogs.DIALOG_ID_EXT_DELETED; message = StringUtils.format( Strings.EXT_DELETED_MESSAGE, StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(doc.file.fullPath)) ); } Dialogs.showModalDialog(dialogId, Strings.EXT_MODIFIED_TITLE, message) .done(function (id) { if (id === Dialogs.DIALOG_BTN_DONTSAVE) { if (toClose) { // Discard - close all editors DocumentManager.notifyFileDeleted(doc.file); result.resolve(); } else { // Discard - load changes from disk reloadDoc(doc) .done(function () { result.resolve(); }) .fail(function (error) { // Unable to load changed version from disk - show error UI showReloadError(error, doc) .always(function () { // After user dismisses, move on to next conflict prompt result.reject(); }); }); } } else { // Cancel - if user doesn't manually save or close, we'll prompt again next // time window is reactivated; // OR programmatically canceled due to _resetPending - we'll skip all // remaining files in the conflicts list (see above) result.resolve(); } }); return promise; } // Begin walking through the conflicts, one at a time return Async.doSequentially(allConflicts, presentConflict, false); } /** * Check to see whether any open files have been modified by an external app since the last time * Brackets synced up with the copy on disk (either by loading or saving the file). For clean * files, we silently upate the editor automatically. For files with unsaved changes, we prompt * the user. */ function syncOpenDocuments() { // We can become "re-entrant" if the user leaves & then returns to Brackets before we're // done -- easy if a prompt dialog is left open. Since the user may have left Brackets to // revert some of the disk changes, etc. we want to cancel the current sync and immediately // begin a new one. We let the orig sync run until the user-visible dialog phase, then // bail; if we're already there we programmatically close the dialog to bail right away. if (_alreadyChecking) { _restartPending = true; // Close dialog if it was open. This will 'unblock' presentConflict(), which bails back // to us immediately upon seeing _restartPending. We then restart the sync - see below Dialogs.cancelModalDialogIfOpen(Dialogs.DIALOG_ID_EXT_CHANGED); Dialogs.cancelModalDialogIfOpen(Dialogs.DIALOG_ID_EXT_DELETED); return; } _alreadyChecking = true; // Syncing proceeds in four phases: // 1) Check all open files for external modifications // 2) Check any other working set entries (that are not open) for deletion, and remove // from working set if deleted // 3) Refresh all Documents that are clean (if file changed on disk) // 4) Close all Documents that are clean (if file deleted on disk) // 5) Prompt about any Documents that are dirty (if file changed/deleted on disk) // Each phase fully completes (asynchronously) before the next one begins. // 1) Check for external modifications var allDocs = DocumentManager.getAllOpenDocuments(); findExternalChanges(allDocs) .done(function () { // 2) Check un-open working set entries for deletion (& "close" if needed) syncUnopenWorkingSet() .always(function () { // If we were unable to check any un-open files for deletion, silently ignore // (after logging to console). This doesn't have any bearing on syncing truly // open Documents (which we've already successfully checked). // 3) Reload clean docs as needed reloadChangedDocs() .always(function () { // 4) Close clean docs as needed // This phase completes synchronously closeDeletedDocs(); // 5) Prompt for dirty editors (conflicts) presentConflicts() .always(function () { if (_restartPending) { // Restart the sync if needed _restartPending = false; _alreadyChecking = false; syncOpenDocuments(); } else { // We're really done! _alreadyChecking = false; // If we showed a dialog, restore focus to editor if (editConflicts.length > 0 || deleteConflicts.length > 0) { EditorManager.focusEditor(); } // (Any errors that ocurred during presentConflicts() have already // shown UI & been dismissed, so there's no fail() handler here) } }); }); // Note: if any auto-reloads failed, we silently ignore (after logging to console) // and we still continue onto phase 4 and try to process those files anyway. // (We'll retry the auto-reloads next time window is activated... and evenually // we'll also be double checking before each Save). }); }).fail(function () { // Unable to fetch timestamps for some reason - silently ignore (after logging to console) // (We'll retry next time window is activated... and evenually we'll also be double // checking before each Save). // We can't go on without knowing which files are dirty, so bail now _alreadyChecking = false; }); } // Define public API exports.syncOpenDocuments = syncOpenDocuments; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Utilities for determining the current "build number" / version */ define('utils/BuildInfoUtils',['require','exports','module','file/NativeFileSystem','file/FileUtils'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, FileUtils = require("file/FileUtils"); var _bracketsSHA = null; var _bracketsAppSHA = null; /** * @return {?string} the Git SHA of the brackets submodule at the time when Brackets launched, * or null if no Git metadata was found on disk. */ function getBracketsSHA() { return _bracketsSHA; } /** * @return {?string} the Git SHA of the brackets-app module at the time when Brackets launched, * or null if no Git metadata was found on disk. */ function getBracketsAppSHA() { return _bracketsAppSHA; } /** * Loads a SHA from Git metadata file. If the file contains a symbolic ref name, follows the ref * and loads the SHA from that file in turn. */ function _loadSHA(path, callback) { var fileEntry = new NativeFileSystem.FileEntry(path); var reader = new NativeFileSystem.FileReader(); var result = new $.Deferred(); // HEAD contains a SHA in detached-head mode; otherwise it contains a relative path // to a file in /refs which in turn contains the SHA fileEntry.file(function (file) { reader.onload = function (event) { var text = event.target.result; if (text.indexOf("ref: ") === 0) { var basePath = path.substr(0, path.lastIndexOf("/")); var refRelPath = text.substr(5).trim(); _loadSHA(basePath + "/" + refRelPath, callback) .pipe(result.resolve, result.reject); } else { result.resolve(text); } }; reader.onerror = function (event) { result.reject(); }; reader.readAsText(file, "utf8"); }); return result.promise(); } function init() { // Look for Git metadata on disk to load the SHAs for 'brackets' and 'brackets-app'. Done on // startup instead of on demand because the version that's currently running is what was // loaded at startup (the src on disk may be updated to a different version later). // Git metadata may be missing (e.g. in the per-sprint ZIP builds) - silently ignore if so. var bracketsSrc = FileUtils.getNativeBracketsDirectoryPath(); var bracketsGitRoot = bracketsSrc + "/../../.git/"; var bracketsSubmoduleRoot_inParent = bracketsGitRoot + "modules/brackets/"; var bracketsSubmoduleRoot_inSubmodule = bracketsSrc + "/../.git/"; _loadSHA(bracketsGitRoot + "HEAD") .done(function (text) { _bracketsAppSHA = text; }); // brackets submodule metadata may be in brackets/.git OR a subfolder of brackets-app/.git, // so try both locations _loadSHA(bracketsSubmoduleRoot_inSubmodule + "HEAD") .done(function (text) { _bracketsSHA = text; }) .fail(function () { _loadSHA(bracketsSubmoduleRoot_inParent + "HEAD") .done(function (text) { _bracketsSHA = text; }); }); } // Define public API exports.init = init; exports.getBracketsSHA = getBracketsSHA; exports.getBracketsAppSHA = getBracketsAppSHA; }); // jslint.js // 2012-01-13 // Copyright (c) 2002 Douglas Crockford (www.JSLint.com) // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // The Software shall be used for Good, not Evil. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // WARNING: JSLint will hurt your feelings. // JSLINT is a global function. It takes two parameters. // var myResult = JSLINT(source, option); // The first parameter is either a string or an array of strings. If it is a // string, it will be split on '\n' or '\r'. If it is an array of strings, it // is assumed that each string represents one line. The source can be a // JavaScript text, or HTML text, or a JSON text, or a CSS text. // The second parameter is an optional object of options that control the // operation of JSLINT. Most of the options are booleans: They are all // optional and have a default value of false. One of the options, predef, // can be an array of names, which will be used to declare global variables, // or an object whose keys are used as global names, with a boolean value // that determines if they are assignable. // If it checks out, JSLINT returns true. Otherwise, it returns false. // If false, you can inspect JSLINT.errors to find out the problems. // JSLINT.errors is an array of objects containing these properties: // { // line : The line (relative to 0) at which the lint was found // character : The character (relative to 0) at which the lint was found // reason : The problem // evidence : The text line in which the problem occurred // raw : The raw message before the details were inserted // a : The first detail // b : The second detail // c : The third detail // d : The fourth detail // } // If a stopping error was found, a null will be the last element of the // JSLINT.errors array. A stopping error means that JSLint was not confident // enough to continue. It does not necessarily mean that the error was // especially heinous. // You can request a Function Report, which shows all of the functions // and the parameters and vars that they use. This can be used to find // implied global variables and other problems. The report is in HTML and // can be inserted in an HTML <body>. // var myReport = JSLINT.report(errors_only); // If errors_only is true, then the report will be limited to only errors. // You can request a data structure that contains JSLint's results. // var myData = JSLINT.data(); // It returns a structure with this form: // { // errors: [ // { // line: NUMBER, // character: NUMBER, // reason: STRING, // evidence: STRING // } // ], // functions: [ // { // name: STRING, // line: NUMBER, // last: NUMBER, // params: [ // { // string: STRING // } // ], // closure: [ // STRING // ], // var: [ // STRING // ], // exception: [ // STRING // ], // outer: [ // STRING // ], // unused: [ // STRING // ], // undef: [ // STRING // ], // global: [ // STRING // ], // label: [ // STRING // ] // } // ], // globals: [ // STRING // ], // member: { // STRING: NUMBER // }, // urls: [ // STRING // ], // json: BOOLEAN // } // Empty arrays will not be included. // You can obtain the parse tree that JSLint constructed while parsing. The // latest tree is kept in JSLINT.tree. A nice stringication can be produced // with // JSON.stringify(JSLINT.tree, [ // 'string', 'arity', 'name', 'first', // 'second', 'third', 'block', 'else' // ], 4)); // JSLint provides three directives. They look like slashstar comments, and // allow for setting options, declaring global variables, and establishing a // set of allowed property names. // These directives respect function scope. // The jslint directive is a special comment that can set one or more options. // The current option set is // anon true, if the space may be omitted in anonymous function declarations // bitwise true, if bitwise operators should be allowed // browser true, if the standard browser globals should be predefined // cap true, if upper case HTML should be allowed // confusion true, if types can be used inconsistently // 'continue' true, if the continuation statement should be tolerated // css true, if CSS workarounds should be tolerated // debug true, if debugger statements should be allowed // devel true, if logging should be allowed (console, alert, etc.) // eqeq true, if == should be allowed // es5 true, if ES5 syntax should be allowed // evil true, if eval should be allowed // forin true, if for in statements need not filter // fragment true, if HTML fragments should be allowed // indent the indentation factor // maxerr the maximum number of errors to allow // maxlen the maximum length of a source line // newcap true, if constructor names capitalization is ignored // node true, if Node.js globals should be predefined // nomen true, if names may have dangling _ // on true, if HTML event handlers should be allowed // passfail true, if the scan should stop on first error // plusplus true, if increment/decrement should be allowed // properties true, if all property names must be declared with /*properties*/ // regexp true, if the . should be allowed in regexp literals // rhino true, if the Rhino environment globals should be predefined // undef true, if variables can be declared out of order // unparam true, if unused parameters should be tolerated // sloppy true, if the pragma is optional // sub true, if all forms of subscript notation are tolerated // vars true, if multiple var statements per function should be allowed // white true, if sloppy whitespace is tolerated // widget true if the Yahoo Widgets globals should be predefined // windows true, if MS Windows-specific globals should be predefined // For example: /*jslint evil: true, nomen: true, regexp: true */ // The properties directive declares an exclusive list of property names. // Any properties named in the program that are not in the list will // produce a warning. // For example: /*properties '\b': string, '\t': string, '\n': string, '\f': string, '\r': string, '!=': boolean, '!==': boolean, '"': string, '%': boolean, '\'': string, '(begin)', '(breakage)': number, '(confusion)': boolean, '(context)': object, '(error)', '(identifier)', '(line)': number, '(loopage)': number, '(name)', '(old_property_type)', '(params)', '(scope)': object, '(token)', '(vars)', '(verb)', '*': boolean, '+': boolean, '-': boolean, '/': *, '<': boolean, '<=': boolean, '==': boolean, '===': boolean, '>': boolean, '>=': boolean, ADSAFE: boolean, Array, Date, E: string, Function, LN10: string, LN2: string, LOG10E: string, LOG2E: string, MAX_VALUE: string, MIN_VALUE: string, NEGATIVE_INFINITY: string, Object, PI: string, POSITIVE_INFINITY: string, SQRT1_2: string, SQRT2: string, '\\': string, a: object, a_label: string, a_not_allowed: string, a_not_defined: string, a_scope: string, abbr: object, acronym: object, address: object, adsafe, adsafe_a: string, adsafe_autocomplete: string, adsafe_bad_id: string, adsafe_div: string, adsafe_fragment: string, adsafe_go: string, adsafe_html: string, adsafe_id: string, adsafe_id_go: string, adsafe_lib: string, adsafe_lib_second: string, adsafe_missing_id: string, adsafe_name_a: string, adsafe_placement: string, adsafe_prefix_a: string, adsafe_script: string, adsafe_source: string, adsafe_subscript_a: string, adsafe_tag: string, all: boolean, already_defined: string, and: string, anon, applet: object, apply: string, approved: array, area: object, arity: string, article: object, aside: object, assign: boolean, assign_exception: string, assignment_function_expression: string, at: number, attribute_case_a: string, audio: object, autocomplete: string, avoid_a: string, b: *, background: array, 'background-attachment': array, 'background-color': array, 'background-image': array, 'background-position': array, 'background-repeat': array, bad_assignment: string, bad_color_a: string, bad_constructor: string, bad_entity: string, bad_html: string, bad_id_a: string, bad_in_a: string, bad_invocation: string, bad_name_a: string, bad_new: string, bad_number: string, bad_operand: string, bad_style: string, bad_type: string, bad_url_a: string, bad_wrap: string, base: object, bdo: object, big: object, bind: string, bitwise: boolean, block: array, blockquote: object, body: object, border: array, 'border-bottom': array, 'border-bottom-color', 'border-bottom-left-radius', 'border-bottom-right-radius', 'border-bottom-style': array, 'border-bottom-width', 'border-collapse': array, 'border-color': array, 'border-left': array, 'border-left-color', 'border-left-style': array, 'border-left-width', 'border-radius', 'border-right': array, 'border-right-color', 'border-right-style': array, 'border-right-width', 'border-spacing': array, 'border-style': array, 'border-top': array, 'border-top-color', 'border-top-left-radius', 'border-top-right-radius', 'border-top-style': array, 'border-top-width', 'border-width': array, bottom: array, br: object, braille: boolean, browser: boolean, button: object, c, call: string, canvas: object, cap, caption: object, 'caption-side': array, ceil: string, center: object, charAt: *, charCodeAt: *, character, cite: object, clear: array, clip: array, closure, cm: boolean, code: object, col: object, colgroup: object, color, combine_var: string, command: object, concat: string, conditional_assignment: string, confusing_a: string, confusing_regexp: string, confusion: boolean, constructor: string, constructor_name_a: string, content: array, continue, control_a: string, 'counter-increment': array, 'counter-reset': array, create: *, css: string, cursor: array, d, dangerous_comment: string, dangling_a: string, data: function object, datalist: object, dd: object, debug, defineProperties: string, defineProperty: string, del: object, deleted: string, details: object, devel: boolean, dfn: object, dialog: object, dir: object, direction: array, display: array, disrupt: boolean, div: object, dl: object, dt: object, duplicate_a: string, edge: string, edition: string, else, em: *, embed: object, embossed: boolean, empty: boolean, 'empty-cells': array, empty_block: string, empty_case: string, empty_class: string, entityify: function, eqeq, errors: array, es5: string, eval, every: string, evidence, evil: string, ex: boolean, exception, exec: *, expected_a: string, expected_a_at_b_c: string, expected_a_b: string, expected_a_b_from_c_d: string, expected_at_a: string, expected_attribute_a: string, expected_attribute_value_a: string, expected_class_a: string, expected_fraction_a: string, expected_id_a: string, expected_identifier_a: string, expected_identifier_a_reserved: string, expected_lang_a: string, expected_linear_a: string, expected_media_a: string, expected_name_a: string, expected_nonstandard_style_attribute: string, expected_number_a: string, expected_operator_a: string, expected_percent_a: string, expected_positive_a: string, expected_pseudo_a: string, expected_selector_a: string, expected_small_a: string, expected_space_a_b: string, expected_string_a: string, expected_style_attribute: string, expected_style_pattern: string, expected_tagname_a: string, expected_type_a: string, f: string, fieldset: object, figure: object, filter: *, first: *, flag, float: array, floor: *, font: *, 'font-family', 'font-size': array, 'font-size-adjust': array, 'font-stretch': array, 'font-style': array, 'font-variant': array, 'font-weight': array, footer: object, for, forEach: *, for_if: string, forin, form: object, fragment, frame: object, frameset: object, freeze: string, from: number, fromCharCode: function, fud: function, funct: object, function, function_block: string, function_eval: string, function_loop: string, function_statement: string, function_strict: string, functions: array, getDate: string, getDay: string, getFullYear: string, getHours: string, getMilliseconds: string, getMinutes: string, getMonth: string, getOwnPropertyDescriptor: string, getOwnPropertyNames: string, getPrototypeOf: string, getSeconds: string, getTime: string, getTimezoneOffset: string, getUTCDate: string, getUTCDay: string, getUTCFullYear: string, getUTCHours: string, getUTCMilliseconds: string, getUTCMinutes: string, getUTCMonth: string, getUTCSeconds: string, getYear: string, global, globals, h1: object, h2: object, h3: object, h4: object, h5: object, h6: object, handheld: boolean, hasOwnProperty: *, head: object, header: object, height: array, hgroup: object, hr: object, 'hta:application': object, html: *, html_confusion_a: string, html_handlers: string, i: object, id: string, identifier: boolean, identifier_function: string, iframe: object, img: object, immed: boolean, implied_evil: string, in, indent: number, indexOf: *, infix_in: string, init: function, input: object, ins: object, insecure_a: string, isAlpha: function, isArray: function boolean, isDigit: function, isExtensible: string, isFrozen: string, isNaN: string, isPrototypeOf: string, isSealed: string, join: *, jslint: function boolean, json: boolean, kbd: object, keygen: object, keys: *, label: object, label_a_b: string, labeled: boolean, lang: string, lastIndex: string, lastIndexOf: *, lbp: number, leading_decimal_a: string, led: function, left: array, legend: object, length: *, 'letter-spacing': array, li: object, lib: boolean, line: number, 'line-height': array, link: object, 'list-style': array, 'list-style-image': array, 'list-style-position': array, 'list-style-type': array, map: *, margin: array, 'margin-bottom', 'margin-left', 'margin-right', 'margin-top', mark: object, 'marker-offset': array, match: function, 'max-height': array, 'max-width': array, maxerr: number, maxlen: number, member: object, menu: object, message, meta: object, meter: object, 'min-height': function, 'min-width': function, missing_a: string, missing_a_after_b: string, missing_option: string, missing_property: string, missing_space_a_b: string, missing_url: string, missing_use_strict: string, mixed: string, mm: boolean, mode: string, move_invocation: string, move_var: string, n: string, name: string, name_function: string, nav: object, nested_comment: string, newcap: boolean, node: boolean, noframes: object, nomen, noscript: object, not: string, not_a_constructor: string, not_a_defined: string, not_a_function: string, not_a_label: string, not_a_scope: string, not_greater: string, now: string, nud: function, number: number, object: object, ol: object, on, opacity, open: boolean, optgroup: object, option: object, outer: regexp, outline: array, 'outline-color': array, 'outline-style': array, 'outline-width', output: object, overflow: array, 'overflow-x': array, 'overflow-y': array, p: object, padding: array, 'padding-bottom': function, 'padding-left': function, 'padding-right': function, 'padding-top': function, 'page-break-after': array, 'page-break-before': array, param: object, parameter_a_get_b: string, parameter_set_a: string, params: array, paren: boolean, parent: string, parse: string, passfail, pc: boolean, plusplus, pop: *, position: array, postscript: boolean, pre: object, predef, preventExtensions: string, print: boolean, progress: object, projection: boolean, properties: boolean, propertyIsEnumerable: string, prototype: string, pt: boolean, push: *, px: boolean, q: object, quote, quotes: array, r: string, radix: string, range: function, raw, read_only: string, reason, redefinition_a: string, reduce: string, reduceRight: string, regexp, replace: function, report: function, reserved: boolean, reserved_a: string, reverse: string, rhino: boolean, right: array, rp: object, rt: object, ruby: object, safe: boolean, samp: object, scanned_a_b: string, screen: boolean, script: object, seal: string, search: function, second: *, section: object, select: object, setDate: string, setDay: string, setFullYear: string, setHours: string, setMilliseconds: string, setMinutes: string, setMonth: string, setSeconds: string, setTime: string, setTimezoneOffset: string, setUTCDate: string, setUTCDay: string, setUTCFullYear: string, setUTCHours: string, setUTCMilliseconds: string, setUTCMinutes: string, setUTCMonth: string, setUTCSeconds: string, setYear: string, shift: *, slash_equal: string, slice: string, sloppy, small: object, some: string, sort: *, source: object, span: object, speech: boolean, splice: string, split: function, src, statement_block: string, stopping: string, strange_loop: string, strict: string, string: string, stringify: string, strong: object, style: *, styleproperty: regexp, sub: object, subscript: string, substr: *, substring: string, sup: object, supplant: function, t: string, table: object, 'table-layout': array, tag_a_in_b: string, tbody: object, td: object, test: *, 'text-align': array, 'text-decoration': array, 'text-indent': function, 'text-shadow': array, 'text-transform': array, textarea: object, tfoot: object, th: object, thead: object, third: array, thru: number, time: object, title: object, toDateString: string, toExponential: string, toFixed: string, toISOString: string, toJSON: string, toLocaleDateString: string, toLocaleLowerCase: string, toLocaleString: string, toLocaleTimeString: string, toLocaleUpperCase: string, toLowerCase: *, toPrecision: string, toString: function, toTimeString: string, toUTCString: string, toUpperCase: *, token: function, too_long: string, too_many: string, top: array, tr: object, trailing_decimal_a: string, tree: string, trim: string, tt: object, tty: boolean, tv: boolean, type: string, type_confusion_a_b: string, u: object, ul: object, unclosed: string, unclosed_comment: string, unclosed_regexp: string, undef: boolean, undefined, unescaped_a: string, unexpected_a: string, unexpected_char_a_b: string, unexpected_comment: string, unexpected_property_a: string, unexpected_space_a_b: string, 'unicode-bidi': array, unnecessary_initialize: string, unnecessary_use: string, unparam, unreachable_a_b: string, unrecognized_style_attribute_a: string, unrecognized_tag_a: string, unsafe: string, unshift: string, unused: array, url: string, urls: array, use_array: string, use_braces: string, use_charAt: string, use_object: string, use_or: string, use_param: string, used_before_a: string, valueOf: string, var: object, var_a_not: string, vars, 'vertical-align': array, video: object, visibility: array, warn: boolean, was: object, weird_assignment: string, weird_condition: string, weird_new: string, weird_program: string, weird_relation: string, weird_ternary: string, white: boolean, 'white-space': array, widget: boolean, width: array, windows: boolean, 'word-spacing': array, 'word-wrap': array, wrap: boolean, wrap_immediate: string, wrap_regexp: string, write_is_wrong: string, writeable: boolean, 'z-index': array */ // The global directive is used to declare global variables that can // be accessed by the program. If a declaration is true, then the variable // is writeable. Otherwise, it is read-only. // We build the application inside a function so that we produce only a single // global variable. That function will be invoked immediately, and its return // value is the JSLINT function itself. That function is also an object that // can contain data and other functions. var JSLINT = (function () { function array_to_object(array, value) { // Make an object from an array of keys and a common value. var i, length = array.length, object = {}; for (i = 0; i < length; i += 1) { object[array[i]] = value; } return object; } var adsafe_id, // The widget's ADsafe id. adsafe_may, // The widget may load approved scripts. adsafe_top, // At the top of the widget script. adsafe_went, // ADSAFE.go has been called. allowed_option = { anon : true, bitwise : true, browser : true, cap : true, confusion : true, 'continue': true, css : true, debug : true, devel : true, eqeq : true, es5 : true, evil : true, forin : true, fragment : true, indent : 10, maxerr : 1000, maxlen : 256, newcap : true, node : true, nomen : true, on : true, passfail : true, plusplus : true, properties: true, regexp : true, rhino : true, undef : true, unparam : true, sloppy : true, sub : true, vars : true, white : true, widget : true, windows : true }, anonname, // The guessed name for anonymous functions. approved, // ADsafe approved urls. // These are operators that should not be used with the ! operator. bang = { '<' : true, '<=' : true, '==' : true, '===': true, '!==': true, '!=' : true, '>' : true, '>=' : true, '+' : true, '-' : true, '*' : true, '/' : true, '%' : true }, // These are property names that should not be permitted in the safe subset. banned = array_to_object([ 'arguments', 'callee', 'caller', 'constructor', 'eval', 'prototype', 'stack', 'unwatch', 'valueOf', 'watch' ], true), begin, // The root token // browser contains a set of global names that are commonly provided by a // web browser environment. browser = array_to_object([ 'clearInterval', 'clearTimeout', 'document', 'event', 'frames', 'history', 'Image', 'localStorage', 'location', 'name', 'navigator', 'Option', 'parent', 'screen', 'sessionStorage', 'setInterval', 'setTimeout', 'Storage', 'window', 'XMLHttpRequest' ], false), // bundle contains the text messages. bundle = { a_label: "'{a}' is a statement label.", a_not_allowed: "'{a}' is not allowed.", a_not_defined: "'{a}' is not defined.", a_scope: "'{a}' used out of scope.", adsafe_a: "ADsafe violation: '{a}'.", adsafe_autocomplete: "ADsafe autocomplete violation.", adsafe_bad_id: "ADSAFE violation: bad id.", adsafe_div: "ADsafe violation: Wrap the widget in a div.", adsafe_fragment: "ADSAFE: Use the fragment option.", adsafe_go: "ADsafe violation: Misformed ADSAFE.go.", adsafe_html: "Currently, ADsafe does not operate on whole HTML " + "documents. It operates on <div> fragments and .js files.", adsafe_id: "ADsafe violation: id does not match.", adsafe_id_go: "ADsafe violation: Missing ADSAFE.id or ADSAFE.go.", adsafe_lib: "ADsafe lib violation.", adsafe_lib_second: "ADsafe: The second argument to lib must be a function.", adsafe_missing_id: "ADSAFE violation: missing ID_.", adsafe_name_a: "ADsafe name violation: '{a}'.", adsafe_placement: "ADsafe script placement violation.", adsafe_prefix_a: "ADsafe violation: An id must have a '{a}' prefix", adsafe_script: "ADsafe script violation.", adsafe_source: "ADsafe unapproved script source.", adsafe_subscript_a: "ADsafe subscript '{a}'.", adsafe_tag: "ADsafe violation: Disallowed tag '{a}'.", already_defined: "'{a}' is already defined.", and: "The '&&' subexpression should be wrapped in parens.", assign_exception: "Do not assign to the exception parameter.", assignment_function_expression: "Expected an assignment or " + "function call and instead saw an expression.", attribute_case_a: "Attribute '{a}' not all lower case.", avoid_a: "Avoid '{a}'.", bad_assignment: "Bad assignment.", bad_color_a: "Bad hex color '{a}'.", bad_constructor: "Bad constructor.", bad_entity: "Bad entity.", bad_html: "Bad HTML string", bad_id_a: "Bad id: '{a}'.", bad_in_a: "Bad for in variable '{a}'.", bad_invocation: "Bad invocation.", bad_name_a: "Bad name: '{a}'.", bad_new: "Do not use 'new' for side effects.", bad_number: "Bad number '{a}'.", bad_operand: "Bad operand.", bad_style: "Bad style.", bad_type: "Bad type.", bad_url_a: "Bad url '{a}'.", bad_wrap: "Do not wrap function literals in parens unless they " + "are to be immediately invoked.", combine_var: "Combine this with the previous 'var' statement.", conditional_assignment: "Expected a conditional expression and " + "instead saw an assignment.", confusing_a: "Confusing use of '{a}'.", confusing_regexp: "Confusing regular expression.", constructor_name_a: "A constructor name '{a}' should start with " + "an uppercase letter.", control_a: "Unexpected control character '{a}'.", css: "A css file should begin with @charset 'UTF-8';", dangling_a: "Unexpected dangling '_' in '{a}'.", dangerous_comment: "Dangerous comment.", deleted: "Only properties should be deleted.", duplicate_a: "Duplicate '{a}'.", empty_block: "Empty block.", empty_case: "Empty case.", empty_class: "Empty class.", es5: "This is an ES5 feature.", evil: "eval is evil.", expected_a: "Expected '{a}'.", expected_a_b: "Expected '{a}' and instead saw '{b}'.", expected_a_b_from_c_d: "Expected '{a}' to match '{b}' from line " + "{c} and instead saw '{d}'.", expected_at_a: "Expected an at-rule, and instead saw @{a}.", expected_a_at_b_c: "Expected '{a}' at column {b}, not column {c}.", expected_attribute_a: "Expected an attribute, and instead saw [{a}].", expected_attribute_value_a: "Expected an attribute value and " + "instead saw '{a}'.", expected_class_a: "Expected a class, and instead saw .{a}.", expected_fraction_a: "Expected a number between 0 and 1 and " + "instead saw '{a}'", expected_id_a: "Expected an id, and instead saw #{a}.", expected_identifier_a: "Expected an identifier and instead saw '{a}'.", expected_identifier_a_reserved: "Expected an identifier and " + "instead saw '{a}' (a reserved word).", expected_linear_a: "Expected a linear unit and instead saw '{a}'.", expected_lang_a: "Expected a lang code, and instead saw :{a}.", expected_media_a: "Expected a CSS media type, and instead saw '{a}'.", expected_name_a: "Expected a name and instead saw '{a}'.", expected_nonstandard_style_attribute: "Expected a non-standard " + "style attribute and instead saw '{a}'.", expected_number_a: "Expected a number and instead saw '{a}'.", expected_operator_a: "Expected an operator and instead saw '{a}'.", expected_percent_a: "Expected a percentage and instead saw '{a}'", expected_positive_a: "Expected a positive number and instead saw '{a}'", expected_pseudo_a: "Expected a pseudo, and instead saw :{a}.", expected_selector_a: "Expected a CSS selector, and instead saw {a}.", expected_small_a: "Expected a small positive integer and instead saw '{a}'", expected_space_a_b: "Expected exactly one space between '{a}' and '{b}'.", expected_string_a: "Expected a string and instead saw {a}.", expected_style_attribute: "Excepted a style attribute, and instead saw '{a}'.", expected_style_pattern: "Expected a style pattern, and instead saw '{a}'.", expected_tagname_a: "Expected a tagName, and instead saw {a}.", expected_type_a: "Expected a type, and instead saw {a}.", for_if: "The body of a for in should be wrapped in an if " + "statement to filter unwanted properties from the prototype.", function_block: "Function statements should not be placed in blocks. " + "Use a function expression or move the statement to the top of " + "the outer function.", function_eval: "The Function constructor is eval.", function_loop: "Don't make functions within a loop.", function_statement: "Function statements are not invocable. " + "Wrap the whole function invocation in parens.", function_strict: "Use the function form of 'use strict'.", html_confusion_a: "HTML confusion in regular expression '<{a}'.", html_handlers: "Avoid HTML event handlers.", identifier_function: "Expected an identifier in an assignment " + "and instead saw a function invocation.", implied_evil: "Implied eval is evil. Pass a function instead of a string.", infix_in: "Unexpected 'in'. Compare with undefined, or use the " + "hasOwnProperty method instead.", insecure_a: "Insecure '{a}'.", isNaN: "Use the isNaN function to compare with NaN.", label_a_b: "Label '{a}' on '{b}' statement.", lang: "lang is deprecated.", leading_decimal_a: "A leading decimal point can be confused with a dot: '.{a}'.", missing_a: "Missing '{a}'.", missing_a_after_b: "Missing '{a}' after '{b}'.", missing_option: "Missing option value.", missing_property: "Missing property name.", missing_space_a_b: "Missing space between '{a}' and '{b}'.", missing_url: "Missing url.", missing_use_strict: "Missing 'use strict' statement.", mixed: "Mixed spaces and tabs.", move_invocation: "Move the invocation into the parens that " + "contain the function.", move_var: "Move 'var' declarations to the top of the function.", name_function: "Missing name in function statement.", nested_comment: "Nested comment.", not: "Nested not.", not_a_constructor: "Do not use {a} as a constructor.", not_a_defined: "'{a}' has not been fully defined yet.", not_a_function: "'{a}' is not a function.", not_a_label: "'{a}' is not a label.", not_a_scope: "'{a}' is out of scope.", not_greater: "'{a}' should not be greater than '{b}'.", parameter_a_get_b: "Unexpected parameter '{a}' in get {b} function.", parameter_set_a: "Expected parameter (value) in set {a} function.", radix: "Missing radix parameter.", read_only: "Read only.", redefinition_a: "Redefinition of '{a}'.", reserved_a: "Reserved name '{a}'.", scanned_a_b: "{a} ({b}% scanned).", slash_equal: "A regular expression literal can be confused with '/='.", statement_block: "Expected to see a statement and instead saw a block.", stopping: "Stopping. ", strange_loop: "Strange loop.", strict: "Strict violation.", subscript: "['{a}'] is better written in dot notation.", tag_a_in_b: "A '<{a}>' must be within '<{b}>'.", too_long: "Line too long.", too_many: "Too many errors.", trailing_decimal_a: "A trailing decimal point can be confused " + "with a dot: '.{a}'.", type: "type is unnecessary.", type_confusion_a_b: "Type confusion: {a} and {b}.", unclosed: "Unclosed string.", unclosed_comment: "Unclosed comment.", unclosed_regexp: "Unclosed regular expression.", unescaped_a: "Unescaped '{a}'.", unexpected_a: "Unexpected '{a}'.", unexpected_char_a_b: "Unexpected character '{a}' in {b}.", unexpected_comment: "Unexpected comment.", unexpected_property_a: "Unexpected /*property*/ '{a}'.", unexpected_space_a_b: "Unexpected space between '{a}' and '{b}'.", unnecessary_initialize: "It is not necessary to initialize '{a}' " + "to 'undefined'.", unnecessary_use: "Unnecessary 'use strict'.", unreachable_a_b: "Unreachable '{a}' after '{b}'.", unrecognized_style_attribute_a: "Unrecognized style attribute '{a}'.", unrecognized_tag_a: "Unrecognized tag '<{a}>'.", unsafe: "Unsafe character.", url: "JavaScript URL.", use_array: "Use the array literal notation [].", use_braces: "Spaces are hard to count. Use {{a}}.", use_charAt: "Use the charAt method.", use_object: "Use the object literal notation {}.", use_or: "Use the || operator.", use_param: "Use a named parameter.", used_before_a: "'{a}' was used before it was defined.", var_a_not: "Variable {a} was not declared correctly.", weird_assignment: "Weird assignment.", weird_condition: "Weird condition.", weird_new: "Weird construction. Delete 'new'.", weird_program: "Weird program.", weird_relation: "Weird relation.", weird_ternary: "Weird ternary.", wrap_immediate: "Wrap an immediate function invocation in parentheses " + "to assist the reader in understanding that the expression " + "is the result of a function, and not the function itself.", wrap_regexp: "Wrap the /regexp/ literal in parens to " + "disambiguate the slash operator.", write_is_wrong: "document.write can be a form of eval." }, comments_off, css_attribute_data, css_any, css_colorData = array_to_object([ "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgreen", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen", "activeborder", "activecaption", "appworkspace", "background", "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "captiontext", "graytext", "highlight", "highlighttext", "inactiveborder", "inactivecaption", "inactivecaptiontext", "infobackground", "infotext", "menu", "menutext", "scrollbar", "threeddarkshadow", "threedface", "threedhighlight", "threedlightshadow", "threedshadow", "window", "windowframe", "windowtext" ], true), css_border_style, css_break, css_lengthData = { '%': true, 'cm': true, 'em': true, 'ex': true, 'in': true, 'mm': true, 'pc': true, 'pt': true, 'px': true }, css_media, css_overflow, descapes = { 'b': '\b', 't': '\t', 'n': '\n', 'f': '\f', 'r': '\r', '"': '"', '/': '/', '\\': '\\' }, devel = array_to_object([ 'alert', 'confirm', 'console', 'Debug', 'opera', 'prompt', 'WSH' ], false), directive, escapes = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\'': '\\\'', '"' : '\\"', '/' : '\\/', '\\': '\\\\' }, funct, // The current function, including the labels used in // the function, as well as (breakage), // (context), (loopage), (name), (params), (token), // (vars), (verb) functionicity = [ 'closure', 'exception', 'global', 'label', 'outer', 'undef', 'unused', 'var' ], functions, // All of the functions global_funct, // The global body global_scope, // The global scope html_tag = { a: {}, abbr: {}, acronym: {}, address: {}, applet: {}, area: {empty: true, parent: ' map '}, article: {}, aside: {}, audio: {}, b: {}, base: {empty: true, parent: ' head '}, bdo: {}, big: {}, blockquote: {}, body: {parent: ' html noframes '}, br: {empty: true}, button: {}, canvas: {parent: ' body p div th td '}, caption: {parent: ' table '}, center: {}, cite: {}, code: {}, col: {empty: true, parent: ' table colgroup '}, colgroup: {parent: ' table '}, command: {parent: ' menu '}, datalist: {}, dd: {parent: ' dl '}, del: {}, details: {}, dialog: {}, dfn: {}, dir: {}, div: {}, dl: {}, dt: {parent: ' dl '}, em: {}, embed: {}, fieldset: {}, figure: {}, font: {}, footer: {}, form: {}, frame: {empty: true, parent: ' frameset '}, frameset: {parent: ' html frameset '}, h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, h6: {}, head: {parent: ' html '}, header: {}, hgroup: {}, hr: {empty: true}, 'hta:application': {empty: true, parent: ' head '}, html: {parent: '*'}, i: {}, iframe: {}, img: {empty: true}, input: {empty: true}, ins: {}, kbd: {}, keygen: {}, label: {}, legend: {parent: ' details fieldset figure '}, li: {parent: ' dir menu ol ul '}, link: {empty: true, parent: ' head '}, map: {}, mark: {}, menu: {}, meta: {empty: true, parent: ' head noframes noscript '}, meter: {}, nav: {}, noframes: {parent: ' html body '}, noscript: {parent: ' body head noframes '}, object: {}, ol: {}, optgroup: {parent: ' select '}, option: {parent: ' optgroup select '}, output: {}, p: {}, param: {empty: true, parent: ' applet object '}, pre: {}, progress: {}, q: {}, rp: {}, rt: {}, ruby: {}, samp: {}, script: {empty: true, parent: ' body div frame head iframe p pre span '}, section: {}, select: {}, small: {}, span: {}, source: {}, strong: {}, style: {parent: ' head ', empty: true}, sub: {}, sup: {}, table: {}, tbody: {parent: ' table '}, td: {parent: ' tr '}, textarea: {}, tfoot: {parent: ' table '}, th: {parent: ' tr '}, thead: {parent: ' table '}, time: {}, title: {parent: ' head '}, tr: {parent: ' table tbody thead tfoot '}, tt: {}, u: {}, ul: {}, 'var': {}, video: {} }, ids, // HTML ids in_block, indent, // infer_statement,// Inference rules for statements is_type = array_to_object([ '*', 'array', 'boolean', 'function', 'number', 'object', 'regexp', 'string' ], true), itself, // JSLint itself json_mode, lex, // the tokenizer lines, lookahead, member, node = array_to_object([ 'Buffer', 'clearInterval', 'clearTimeout', 'console', 'exports', 'global', 'module', 'process', 'querystring', 'require', 'setInterval', 'setTimeout', '__dirname', '__filename' ], false), node_js, numbery = array_to_object(['indexOf', 'lastIndexOf', 'search'], true), next_token, option, predefined, // Global variables defined by option prereg, prev_token, property_type, regexp_flag = array_to_object(['g', 'i', 'm'], true), return_this = function return_this() { return this; }, rhino = array_to_object([ 'defineClass', 'deserialize', 'gc', 'help', 'load', 'loadClass', 'print', 'quit', 'readFile', 'readUrl', 'runCommand', 'seal', 'serialize', 'spawn', 'sync', 'toint32', 'version' ], false), scope, // An object containing an object for each variable in scope semicolon_coda = array_to_object([';', '"', '\'', ')'], true), src, stack, // standard contains the global names that are provided by the // ECMAScript standard. standard = array_to_object([ 'Array', 'Boolean', 'Date', 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'Error', 'eval', 'EvalError', 'Function', 'isFinite', 'isNaN', 'JSON', 'Math', 'Number', 'Object', 'parseInt', 'parseFloat', 'RangeError', 'ReferenceError', 'RegExp', 'String', 'SyntaxError', 'TypeError', 'URIError' ], false), standard_property_type = { E : 'number', LN2 : 'number', LN10 : 'number', LOG2E : 'number', LOG10E : 'number', MAX_VALUE : 'number', MIN_VALUE : 'number', NEGATIVE_INFINITY : 'number', PI : 'number', POSITIVE_INFINITY : 'number', SQRT1_2 : 'number', SQRT2 : 'number', apply : 'function', bind : 'function function', call : 'function', ceil : 'function number', charAt : 'function string', concat : 'function', constructor : 'function object', create : 'function object', defineProperty : 'function object', defineProperties : 'function object', every : 'function boolean', exec : 'function array', filter : 'function array', floor : 'function number', forEach : 'function', freeze : 'function object', getDate : 'function number', getDay : 'function number', getFullYear : 'function number', getHours : 'function number', getMilliseconds : 'function number', getMinutes : 'function number', getMonth : 'function number', getOwnPropertyDescriptor : 'function object', getOwnPropertyNames : 'function array', getPrototypeOf : 'function object', getSeconds : 'function number', getTime : 'function number', getTimezoneOffset : 'function number', getUTCDate : 'function number', getUTCDay : 'function number', getUTCFullYear : 'function number', getUTCHours : 'function number', getUTCMilliseconds : 'function number', getUTCMinutes : 'function number', getUTCMonth : 'function number', getUTCSeconds : 'function number', getYear : 'function number', hasOwnProperty : 'function boolean', indexOf : 'function number', isExtensible : 'function boolean', isFrozen : 'function boolean', isPrototypeOf : 'function boolean', isSealed : 'function boolean', join : 'function string', keys : 'function array', lastIndexOf : 'function number', lastIndex : 'number', length : 'number', map : 'function array', now : 'function number', parse : 'function', pop : 'function', preventExtensions : 'function object', propertyIsEnumerable: 'function boolean', prototype : 'object', push : 'function number', reduce : 'function', reduceRight : 'function', reverse : 'function', seal : 'function object', setDate : 'function', setDay : 'function', setFullYear : 'function', setHours : 'function', setMilliseconds : 'function', setMinutes : 'function', setMonth : 'function', setSeconds : 'function', setTime : 'function', setTimezoneOffset : 'function', setUTCDate : 'function', setUTCDay : 'function', setUTCFullYear : 'function', setUTCHours : 'function', setUTCMilliseconds : 'function', setUTCMinutes : 'function', setUTCMonth : 'function', setUTCSeconds : 'function', setYear : 'function', shift : 'function', slice : 'function', some : 'function boolean', sort : 'function', splice : 'function', stringify : 'function string', substr : 'function string', substring : 'function string', test : 'function boolean', toDateString : 'function string', toExponential : 'function string', toFixed : 'function string', toJSON : 'function', toISOString : 'function string', toLocaleDateString : 'function string', toLocaleLowerCase : 'function string', toLocaleUpperCase : 'function string', toLocaleString : 'function string', toLocaleTimeString : 'function string', toLowerCase : 'function string', toPrecision : 'function string', toTimeString : 'function string', toUpperCase : 'function string', toUTCString : 'function string', trim : 'function string', unshift : 'function number', valueOf : 'function' }, strict_mode, syntax = {}, tab, token, // type_state_change, urls, var_mode, warnings, // widget contains the global names which are provided to a Yahoo // (fna Konfabulator) widget. widget = array_to_object([ 'alert', 'animator', 'appleScript', 'beep', 'bytesToUIString', 'Canvas', 'chooseColor', 'chooseFile', 'chooseFolder', 'closeWidget', 'COM', 'convertPathToHFS', 'convertPathToPlatform', 'CustomAnimation', 'escape', 'FadeAnimation', 'filesystem', 'Flash', 'focusWidget', 'form', 'FormField', 'Frame', 'HotKey', 'Image', 'include', 'isApplicationRunning', 'iTunes', 'konfabulatorVersion', 'log', 'md5', 'MenuItem', 'MoveAnimation', 'openURL', 'play', 'Point', 'popupMenu', 'preferenceGroups', 'preferences', 'print', 'prompt', 'random', 'Rectangle', 'reloadWidget', 'ResizeAnimation', 'resolvePath', 'resumeUpdates', 'RotateAnimation', 'runCommand', 'runCommandInBg', 'saveAs', 'savePreferences', 'screen', 'ScrollBar', 'showWidgetPreferences', 'sleep', 'speak', 'Style', 'suppressUpdates', 'system', 'tellWidget', 'Text', 'TextArea', 'Timer', 'unescape', 'updateNow', 'URL', 'Web', 'widget', 'Window', 'XMLDOM', 'XMLHttpRequest', 'yahooCheckLogin', 'yahooLogin', 'yahooLogout' ], true), windows = array_to_object([ 'ActiveXObject', 'CScript', 'Debug', 'Enumerator', 'System', 'VBArray', 'WScript', 'WSH' ], false), // xmode is used to adapt to the exceptions in html parsing. // It can have these states: // '' .js script file // 'html' // 'outer' // 'script' // 'style' // 'scriptstring' // 'styleproperty' xmode, xquote, // Regular expressions. Some of these are stupidly long. // unsafe comment or string ax = /@cc|<\/?|script|\]\s*\]|<\s*!|</i, // carriage return, or carriage return linefeed crx = /\r/g, crlfx = /\r\n/g, // unsafe characters that are silently deleted by one or more browsers cx = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/, // query characters for ids dx = /[\[\]\/\\"'*<>.&:(){}+=#]/, // html token hx = /^\s*(['"=>\/&#]|<(?:\/|\!(?:--)?)?|[a-zA-Z][a-zA-Z0-9_\-:]*|[0-9]+|--)/, // identifier ix = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/, // javascript url jx = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i, // star slash lx = /\*\/|\/\*/, // characters in strings that need escapement nx = /[\u0000-\u001f'\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, // outer html token ox = /[>&]|<[\/!]?|--/, // attributes characters qx = /[^a-zA-Z0-9+\-_\/ ]/, // style sx = /^\s*([{}:#%.=,>+\[\]@()"';]|[*$\^~]=|[a-zA-Z_][a-zA-Z0-9_\-]*|[0-9]+|<\/|\/\*)/, ssx = /^\s*([@#!"'};:\-%.=,+\[\]()*_]|[a-zA-Z][a-zA-Z0-9._\-]*|\/\*?|\d+(?:\.\d+)?|<\/)/, // token tx = /^\s*([(){}\[\]\?.,:;'"~#@`]|={1,3}|\/(\*(jslint|properties|property|members?|globals?)?|=|\/)?|\*[\/=]?|\+(?:=|\++)?|-(?:=|-+)?|[\^%]=?|&[&=]?|\|[|=]?|>{1,3}=?|<(?:[\/=!]|\!(\[|--)?|<=?)?|\!={0,2}|[a-zA-Z_$][a-zA-Z0-9_$]*|[0-9]+(?:[xX][0-9a-fA-F]+|\.[0-9]*)?(?:[eE][+\-]?[0-9]+)?)/, // url badness ux = /&|\+|\u00AD|\.\.|\/\*|%[^;]|base64|url|expression|data|mailto|script/i, rx = { outer: hx, html: hx, style: sx, styleproperty: ssx }; function F() {} // Used by Object.create // Provide critical ES5 functions to ES3. if (typeof Array.prototype.filter !== 'function') { Array.prototype.filter = function (f) { var i, length = this.length, result = [], value; for (i = 0; i < length; i += 1) { try { value = this[i]; if (f(value)) { result.push(value); } } catch (ignore) { } } return result; }; } if (typeof Array.prototype.forEach !== 'function') { Array.prototype.forEach = function (f) { var i, length = this.length; for (i = 0; i < length; i += 1) { try { f(this[i]); } catch (ignore) { } } }; } if (typeof Array.isArray !== 'function') { Array.isArray = function (o) { return Object.prototype.toString.apply(o) === '[object Array]'; }; } if (!Object.prototype.hasOwnProperty.call(Object, 'create')) { Object.create = function (o) { F.prototype = o; return new F(); }; } if (typeof Object.keys !== 'function') { Object.keys = function (o) { var array = [], key; for (key in o) { if (Object.prototype.hasOwnProperty.call(o, key)) { array.push(key); } } return array; }; } if (typeof String.prototype.entityify !== 'function') { String.prototype.entityify = function () { return this .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); }; } if (typeof String.prototype.isAlpha !== 'function') { String.prototype.isAlpha = function () { return (this >= 'a' && this <= 'z\uffff') || (this >= 'A' && this <= 'Z\uffff'); }; } if (typeof String.prototype.isDigit !== 'function') { String.prototype.isDigit = function () { return (this >= '0' && this <= '9'); }; } if (typeof String.prototype.supplant !== 'function') { String.prototype.supplant = function (o) { return this.replace(/\{([^{}]*)\}/g, function (a, b) { var replacement = o[b]; return typeof replacement === 'string' || typeof replacement === 'number' ? replacement : a; }); }; } function sanitize(a) { // Escapify a troublesome character. return escapes[a] || '\\u' + ('0000' + a.charCodeAt().toString(16)).slice(-4); } function add_to_predefined(group) { Object.keys(group).forEach(function (name) { predefined[name] = group[name]; }); } function assume() { if (!option.safe) { if (option.rhino) { add_to_predefined(rhino); option.rhino = false; } if (option.devel) { add_to_predefined(devel); option.devel = false; } if (option.browser) { add_to_predefined(browser); option.browser = false; } if (option.windows) { add_to_predefined(windows); option.windows = false; } if (option.node) { add_to_predefined(node); option.node = false; node_js = true; } if (option.widget) { add_to_predefined(widget); option.widget = false; } } if (option.type) { option.confusion = true; } } // Produce an error warning. function artifact(tok) { if (!tok) { tok = next_token; } return tok.number || tok.string; } function quit(message, line, character) { throw { name: 'JSLintError', line: line, character: character, message: bundle.scanned_a_b.supplant({ a: message, b: Math.floor((line / lines.length) * 100) }) }; } function warn(message, offender, a, b, c, d) { var character, line, warning; offender = offender || next_token; // `~ line = offender.line || 0; character = offender.from || 0; warning = { id: '(error)', raw: bundle[message] || message, evidence: lines[line - 1] || '', line: line, character: character, a: a || (offender.id === '(number)' ? String(offender.number) : offender.string), b: b, c: c, d: d }; warning.reason = warning.raw.supplant(warning); JSLINT.errors.push(warning); if (option.passfail) { quit(bundle.stopping, line, character); } warnings += 1; if (warnings >= option.maxerr) { quit(bundle.too_many, line, character); } return warning; } function warn_at(message, line, character, a, b, c, d) { return warn(message, { line: line, from: character }, a, b, c, d); } function stop(message, offender, a, b, c, d) { var warning = warn(message, offender, a, b, c, d); quit(bundle.stopping, warning.line, warning.character); } function stop_at(message, line, character, a, b, c, d) { return stop(message, { line: line, from: character }, a, b, c, d); } function expected_at(at) { if (!option.white && next_token.from !== at) { warn('expected_a_at_b_c', next_token, '', at, next_token.from); } } function aint(it, name, expected) { if (it[name] !== expected) { warn('expected_a_b', it, expected, it[name]); return true; } else { return false; } } // lexical analysis and token construction lex = (function lex() { var character, c, from, length, line, pos, source_row; // Private lex methods function next_line() { var at; if (line >= lines.length) { return false; } character = 1; source_row = lines[line]; line += 1; at = source_row.search(/ \t/); if (at >= 0) { warn_at('mixed', line, at + 1); } source_row = source_row.replace(/\t/g, tab); at = source_row.search(cx); if (at >= 0) { warn_at('unsafe', line, at); } if (option.maxlen && option.maxlen < source_row.length) { warn_at('too_long', line, source_row.length); } return true; } // Produce a token object. The token inherits from a syntax symbol. function it(type, value) { var id, the_token; if (type === '(string)' || type === '(range)') { if (jx.test(value)) { warn_at('url', line, from); } } the_token = Object.create(syntax[( type === '(punctuator)' || (type === '(identifier)' && Object.prototype.hasOwnProperty.call(syntax, value)) ? value : type )] || syntax['(error)']); if (type === '(identifier)') { the_token.identifier = true; if (value === '__iterator__' || value === '__proto__') { stop_at('reserved_a', line, from, value); } else if (!option.nomen && (value.charAt(0) === '_' || value.charAt(value.length - 1) === '_')) { warn_at('dangling_a', line, from, value); } } if (type === '(number)') { the_token.number = +value; } else if (value !== undefined) { the_token.string = String(value); } the_token.line = line; the_token.from = from; the_token.thru = character; id = the_token.id; prereg = id && ( ('(,=:[!&|?{};'.indexOf(id.charAt(id.length - 1)) >= 0) || id === 'return' || id === 'case' ); return the_token; } function match(x) { var exec = x.exec(source_row), first; if (exec) { length = exec[0].length; first = exec[1]; c = first.charAt(0); source_row = source_row.slice(length); from = character + length - first.length; character += length; return first; } } function string(x) { var c, pos = 0, r = '', result; function hex(n) { var i = parseInt(source_row.substr(pos + 1, n), 16); pos += n; if (i >= 32 && i <= 126 && i !== 34 && i !== 92 && i !== 39) { warn_at('unexpected_a', line, character, '\\'); } character += n; c = String.fromCharCode(i); } if (json_mode && x !== '"') { warn_at('expected_a', line, character, '"'); } if (xquote === x || (xmode === 'scriptstring' && !xquote)) { return it('(punctuator)', x); } for (;;) { while (pos >= source_row.length) { pos = 0; if (xmode !== 'html' || !next_line()) { stop_at('unclosed', line, from); } } c = source_row.charAt(pos); if (c === x) { character += 1; source_row = source_row.slice(pos + 1); result = it('(string)', r); result.quote = x; return result; } if (c < ' ') { if (c === '\n' || c === '\r') { break; } warn_at('control_a', line, character + pos, source_row.slice(0, pos)); } else if (c === xquote) { warn_at('bad_html', line, character + pos); } else if (c === '<') { if (option.safe && xmode === 'html') { warn_at('adsafe_a', line, character + pos, c); } else if (source_row.charAt(pos + 1) === '/' && (xmode || option.safe)) { warn_at('expected_a_b', line, character, '<\\/', '</'); } else if (source_row.charAt(pos + 1) === '!' && (xmode || option.safe)) { warn_at('unexpected_a', line, character, '<!'); } } else if (c === '\\') { if (xmode === 'html') { if (option.safe) { warn_at('adsafe_a', line, character + pos, c); } } else if (xmode === 'styleproperty') { pos += 1; character += 1; c = source_row.charAt(pos); if (c !== x) { warn_at('unexpected_a', line, character, '\\'); } } else { pos += 1; character += 1; c = source_row.charAt(pos); switch (c) { case '': if (!option.es5) { warn_at('es5', line, character); } next_line(); pos = -1; break; case xquote: warn_at('bad_html', line, character + pos); break; case '\'': if (json_mode) { warn_at('unexpected_a', line, character, '\\\''); } break; case 'u': hex(4); break; case 'v': if (json_mode) { warn_at('unexpected_a', line, character, '\\v'); } c = '\v'; break; case 'x': if (json_mode) { warn_at('unexpected_a', line, character, '\\x'); } hex(2); break; default: c = descapes[c]; if (typeof c !== 'string') { warn_at('unexpected_a', line, character, '\\'); } } } } r += c; character += 1; pos += 1; } } function number(snippet) { var digit; if (xmode !== 'style' && xmode !== 'styleproperty' && source_row.charAt(0).isAlpha()) { warn_at('expected_space_a_b', line, character, c, source_row.charAt(0)); } if (c === '0') { digit = snippet.charAt(1); if (digit.isDigit()) { if (token.id !== '.' && xmode !== 'styleproperty') { warn_at('unexpected_a', line, character, snippet); } } else if (json_mode && (digit === 'x' || digit === 'X')) { warn_at('unexpected_a', line, character, '0x'); } } if (snippet.slice(snippet.length - 1) === '.') { warn_at('trailing_decimal_a', line, character, snippet); } if (xmode !== 'style') { digit = +snippet; if (!isFinite(digit)) { warn_at('bad_number', line, character, snippet); } snippet = digit; } return it('(number)', snippet); } function comment(snippet) { if (comments_off || src || (xmode && xmode !== 'script' && xmode !== 'style' && xmode !== 'styleproperty')) { warn_at('unexpected_comment', line, character); } else if (xmode === 'script' && /<\//i.test(source_row)) { warn_at('unexpected_a', line, character, '<\/'); } else if (option.safe && ax.test(snippet)) { warn_at('dangerous_comment', line, character); } } function regexp() { var b, bit, captures = 0, depth = 0, flag = '', high, letter, length = 0, low, potential, quote, result; for (;;) { b = true; c = source_row.charAt(length); length += 1; switch (c) { case '': stop_at('unclosed_regexp', line, from); return; case '/': if (depth > 0) { warn_at('unescaped_a', line, from + length, '/'); } c = source_row.slice(0, length - 1); potential = Object.create(regexp_flag); for (;;) { letter = source_row.charAt(length); if (potential[letter] !== true) { break; } potential[letter] = false; length += 1; flag += letter; } if (source_row.charAt(length).isAlpha()) { stop_at('unexpected_a', line, from, source_row.charAt(length)); } character += length; source_row = source_row.slice(length); quote = source_row.charAt(0); if (quote === '/' || quote === '*') { stop_at('confusing_regexp', line, from); } result = it('(regexp)', c); result.flag = flag; return result; case '\\': c = source_row.charAt(length); if (c < ' ') { warn_at('control_a', line, from + length, String(c)); } else if (c === '<') { warn_at(bundle.unexpected_a, line, from + length, '\\'); } length += 1; break; case '(': depth += 1; b = false; if (source_row.charAt(length) === '?') { length += 1; switch (source_row.charAt(length)) { case ':': case '=': case '!': length += 1; break; default: warn_at(bundle.expected_a_b, line, from + length, ':', source_row.charAt(length)); } } else { captures += 1; } break; case '|': b = false; break; case ')': if (depth === 0) { warn_at('unescaped_a', line, from + length, ')'); } else { depth -= 1; } break; case ' ': pos = 1; while (source_row.charAt(length) === ' ') { length += 1; pos += 1; } if (pos > 1) { warn_at('use_braces', line, from + length, pos); } break; case '[': c = source_row.charAt(length); if (c === '^') { length += 1; if (!option.regexp) { warn_at('insecure_a', line, from + length, c); } else if (source_row.charAt(length) === ']') { stop_at('unescaped_a', line, from + length, '^'); } } bit = false; if (c === ']') { warn_at('empty_class', line, from + length - 1); bit = true; } klass: do { c = source_row.charAt(length); length += 1; switch (c) { case '[': case '^': warn_at('unescaped_a', line, from + length, c); bit = true; break; case '-': if (bit) { bit = false; } else { warn_at('unescaped_a', line, from + length, '-'); bit = true; } break; case ']': if (!bit) { warn_at('unescaped_a', line, from + length - 1, '-'); } break klass; case '\\': c = source_row.charAt(length); if (c < ' ') { warn_at(bundle.control_a, line, from + length, String(c)); } else if (c === '<') { warn_at(bundle.unexpected_a, line, from + length, '\\'); } length += 1; bit = true; break; case '/': warn_at('unescaped_a', line, from + length - 1, '/'); bit = true; break; case '<': if (xmode === 'script') { c = source_row.charAt(length); if (c === '!' || c === '/') { warn_at(bundle.html_confusion_a, line, from + length, c); } } bit = true; break; default: bit = true; } } while (c); break; case '.': if (!option.regexp) { warn_at('insecure_a', line, from + length, c); } break; case ']': case '?': case '{': case '}': case '+': case '*': warn_at('unescaped_a', line, from + length, c); break; case '<': if (xmode === 'script') { c = source_row.charAt(length); if (c === '!' || c === '/') { warn_at(bundle.html_confusion_a, line, from + length, c); } } break; } if (b) { switch (source_row.charAt(length)) { case '?': case '+': case '*': length += 1; if (source_row.charAt(length) === '?') { length += 1; } break; case '{': length += 1; c = source_row.charAt(length); if (c < '0' || c > '9') { warn_at(bundle.expected_number_a, line, from + length, c); } length += 1; low = +c; for (;;) { c = source_row.charAt(length); if (c < '0' || c > '9') { break; } length += 1; low = +c + (low * 10); } high = low; if (c === ',') { length += 1; high = Infinity; c = source_row.charAt(length); if (c >= '0' && c <= '9') { length += 1; high = +c; for (;;) { c = source_row.charAt(length); if (c < '0' || c > '9') { break; } length += 1; high = +c + (high * 10); } } } if (source_row.charAt(length) !== '}') { warn_at(bundle.expected_a_b, line, from + length, '}', c); } else { length += 1; } if (source_row.charAt(length) === '?') { length += 1; } if (low > high) { warn_at(bundle.not_greater, line, from + length, low, high); } break; } } } c = source_row.slice(0, length - 1); character += length; source_row = source_row.slice(length); return it('(regexp)', c); } // Public lex methods return { init: function (source) { if (typeof source === 'string') { lines = source .replace(crlfx, '\n') .replace(crx, '\n') .split('\n'); } else { lines = source; } line = 0; next_line(); from = 1; }, range: function (begin, end) { var c, value = ''; from = character; if (source_row.charAt(0) !== begin) { stop_at('expected_a_b', line, character, begin, source_row.charAt(0)); } for (;;) { source_row = source_row.slice(1); character += 1; c = source_row.charAt(0); switch (c) { case '': stop_at('missing_a', line, character, c); break; case end: source_row = source_row.slice(1); character += 1; return it('(range)', value); case xquote: case '\\': warn_at('unexpected_a', line, character, c); break; } value += c; } }, // token -- this is called by advance to get the next token. token: function () { var c, i, snippet; for (;;) { while (!source_row) { if (!next_line()) { return it('(end)'); } } while (xmode === 'outer') { i = source_row.search(ox); if (i === 0) { break; } else if (i > 0) { character += 1; source_row = source_row.slice(i); break; } else { if (!next_line()) { return it('(end)', ''); } } } snippet = match(rx[xmode] || tx); if (!snippet) { if (source_row) { if (source_row.charAt(0) === ' ') { if (!option.white) { warn_at('unexpected_a', line, character, '(space)'); } character += 1; source_row = ''; } else { stop_at('unexpected_a', line, character, source_row.charAt(0)); } } } else { // identifier c = snippet.charAt(0); if (c.isAlpha() || c === '_' || c === '$') { return it('(identifier)', snippet); } // number if (c.isDigit()) { return number(snippet); } switch (snippet) { // string case '"': case "'": return string(snippet); // // comment case '//': comment(source_row); source_row = ''; break; // /* comment case '/*': for (;;) { i = source_row.search(lx); if (i >= 0) { break; } comment(source_row); if (!next_line()) { stop_at('unclosed_comment', line, character); } } comment(source_row.slice(0, i)); character += i + 2; if (source_row.charAt(i) === '/') { stop_at('nested_comment', line, character); } source_row = source_row.slice(i + 2); break; case '': break; // / case '/': if (token.id === '/=') { stop_at( bundle.slash_equal, line, from ); } return prereg ? regexp() : it('(punctuator)', snippet); // punctuator case '<!--': length = line; // c = character; for (;;) { i = source_row.indexOf('--'); if (i >= 0) { break; } i = source_row.indexOf('<!'); if (i >= 0) { stop_at('nested_comment', line, character + i); } if (!next_line()) { stop_at('unclosed_comment', length, c); } } length = source_row.indexOf('<!'); if (length >= 0 && length < i) { stop_at('nested_comment', line, character + length); } character += i; if (source_row.charAt(i + 2) !== '>') { stop_at('expected_a', line, character, '-->'); } character += 3; source_row = source_row.slice(i + 3); break; case '#': if (xmode === 'html' || xmode === 'styleproperty') { for (;;) { c = source_row.charAt(0); if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { break; } character += 1; source_row = source_row.slice(1); snippet += c; } if (snippet.length !== 4 && snippet.length !== 7) { warn_at('bad_color_a', line, from + length, snippet); } return it('(color)', snippet); } return it('(punctuator)', snippet); default: if (xmode === 'outer' && c === '&') { character += 1; source_row = source_row.slice(1); for (;;) { c = source_row.charAt(0); character += 1; source_row = source_row.slice(1); if (c === ';') { break; } if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c === '#')) { stop_at('bad_entity', line, from + length, character); } } break; } return it('(punctuator)', snippet); } } } } }; }()); function add_label(token, kind, name) { // Define the symbol in the current function in the current scope. name = name || token.string; // Global variables cannot be created in the safe subset. If a global variable // already exists, do nothing. If it is predefined, define it. if (funct === global_funct) { if (option.safe) { warn('adsafe_a', token, name); } if (typeof global_funct[name] !== 'string') { token.writeable = typeof predefined[name] === 'boolean' ? predefined[name] : true; token.funct = funct; global_scope[name] = token; } if (kind === 'becoming') { kind = 'var'; } // Ordinary variables. } else { // Warn if the variable already exists. if (typeof funct[name] === 'string') { if (funct[name] === 'undef') { if (!option.undef) { warn('used_before_a', token, name); } kind = 'var'; } else { warn('already_defined', token, name); } } else { // Add the symbol to the current function. token.funct = funct; token.writeable = true; scope[name] = token; } } funct[name] = kind; } function peek(distance) { // Peek ahead to a future token. The distance is how far ahead to look. The // default is the next token. var found, slot = 0; distance = distance || 0; while (slot <= distance) { found = lookahead[slot]; if (!found) { found = lookahead[slot] = lex.token(); } slot += 1; } return found; } function advance(id, match) { // Produce the next token, also looking for programming errors. if (indent) { // If indentation checking was requested, then inspect all of the line breakings. // The var statement is tricky because the names might be aligned or not. We // look at the first line break after the var to determine the programmer's // intention. if (var_mode && next_token.line !== token.line) { if ((var_mode !== indent || !next_token.edge) && next_token.from === indent.at - (next_token.edge ? option.indent : 0)) { var dent = indent; for (;;) { dent.at -= option.indent; if (dent === var_mode) { break; } dent = dent.was; } dent.open = false; } var_mode = null; } if (next_token.id === '?' && indent.mode === ':' && token.line !== next_token.line) { indent.at -= option.indent; } if (indent.open) { // If the token is an edge. if (next_token.edge) { if (next_token.edge === 'label') { expected_at(1); } else if (next_token.edge === 'case' || indent.mode === 'statement') { expected_at(indent.at - option.indent); } else if (indent.mode !== 'array' || next_token.line !== token.line) { expected_at(indent.at); } // If the token is not an edge, but is the first token on the line. } else if (next_token.line !== token.line) { if (next_token.from < indent.at + (indent.mode === 'expression' ? 0 : option.indent)) { expected_at(indent.at + option.indent); } indent.wrap = true; } } else if (next_token.line !== token.line) { if (next_token.edge) { expected_at(indent.at); } else { indent.wrap = true; if (indent.mode === 'statement' || indent.mode === 'var') { expected_at(indent.at + option.indent); } else if (next_token.from < indent.at + (indent.mode === 'expression' ? 0 : option.indent)) { expected_at(indent.at + option.indent); } } } } switch (token.id) { case '(number)': if (next_token.id === '.') { warn('trailing_decimal_a'); } break; case '-': if (next_token.id === '-' || next_token.id === '--') { warn('confusing_a'); } break; case '+': if (next_token.id === '+' || next_token.id === '++') { warn('confusing_a'); } break; } if (token.id === '(string)' || token.identifier) { anonname = token.string; } if (id && next_token.id !== id) { if (match) { warn('expected_a_b_from_c_d', next_token, id, match.id, match.line, artifact()); } else if (!next_token.identifier || next_token.string !== id) { warn('expected_a_b', next_token, id, artifact()); } } prev_token = token; token = next_token; next_token = lookahead.shift() || lex.token(); } function advance_identifier(string) { if (next_token.identifier && next_token.string === string) { advance(); } else { warn('expected_a_b', next_token, string, artifact()); } } function do_safe() { if (option.adsafe) { option.safe = true; } if (option.safe) { option.browser = option['continue'] = option.css = option.debug = option.devel = option.evil = option.forin = option.newcap = option.nomen = option.on = option.rhino = option.sloppy = option.sub = option.undef = option.widget = option.windows = false; delete predefined.Array; delete predefined.Date; delete predefined.Function; delete predefined.Object; delete predefined['eval']; add_to_predefined({ ADSAFE: false, lib: false }); } } function do_globals() { var name, writeable; for (;;) { if (next_token.id !== '(string)' && !next_token.identifier) { return; } name = next_token.string; advance(); writeable = false; if (next_token.id === ':') { advance(':'); switch (next_token.id) { case 'true': writeable = predefined[name] !== false; advance('true'); break; case 'false': advance('false'); break; default: stop('unexpected_a'); } } predefined[name] = writeable; if (next_token.id !== ',') { return; } advance(','); } } function do_jslint() { var name, value; while (next_token.id === '(string)' || next_token.identifier) { name = next_token.string; if (!allowed_option[name]) { stop('unexpected_a'); } advance(); if (next_token.id !== ':') { stop('expected_a_b', next_token, ':', artifact()); } advance(':'); if (typeof allowed_option[name] === 'number') { value = next_token.number; if (value > allowed_option[name] || value <= 0 || Math.floor(value) !== value) { stop('expected_small_a'); } option[name] = value; } else { if (next_token.id === 'true') { option[name] = true; } else if (next_token.id === 'false') { option[name] = false; } else { stop('unexpected_a'); } } advance(); if (next_token.id === ',') { advance(','); } } assume(); } function do_properties() { var name, type; option.properties = true; if (!funct['(old_property_type)']) { funct['(old_property_type)'] = property_type; property_type = Object.create(property_type); } for (;;) { if (next_token.id !== '(string)' && !next_token.identifier) { return; } name = next_token.string; type = ''; advance(); if (next_token.id === ':') { advance(':'); if (next_token.id === 'function') { advance('function'); if (is_type[next_token.string] === true) { type = 'function ' + next_token.string; advance(); } else { type = 'function'; } } else { type = next_token.string; if (is_type[type] !== true) { warn('expected_type_a', next_token); type = ''; } advance(); } } property_type[name] = type; if (next_token.id !== ',') { return; } advance(','); } } directive = function directive() { var command = this.id, old_comments_off = comments_off, old_indent = indent; comments_off = true; indent = null; if (next_token.line === token.line && next_token.from === token.thru) { warn('missing_space_a_b', next_token, artifact(token), artifact()); } if (lookahead.length > 0) { warn('unexpected_a', this); } switch (command) { case '/*properties': case '/*property': case '/*members': case '/*member': do_properties(); break; case '/*jslint': if (option.safe) { warn('adsafe_a', this); } do_jslint(); break; case '/*globals': case '/*global': if (option.safe) { warn('adsafe_a', this); } do_globals(); break; default: stop('unexpected_a', this); } comments_off = old_comments_off; advance('*/'); indent = old_indent; }; // Indentation intention function edge(mode) { next_token.edge = indent ? indent.open && (mode || 'edge') : ''; } function step_in(mode) { var open; if (typeof mode === 'number') { indent = { at: +mode, open: true, was: indent }; } else if (mode === 'statement') { indent = { at: indent.at, open: true, was: indent }; } else if (!indent) { indent = { at: 1, mode: 'statement', open: true }; } else { open = mode === 'var' || next_token.line !== token.line; indent = { at: (open || mode === 'control' ? indent.at + option.indent : indent.at) + (indent.wrap ? option.indent : 0), mode: mode, open: open, was: indent }; if (mode === 'var' && open) { var_mode = indent; } } } function step_out(id, symbol) { if (id) { if (indent && indent.open) { indent.at -= option.indent; edge(); } advance(id, symbol); } if (indent) { indent = indent.was; } } // Functions for conformance of whitespace. function one_space(left, right) { left = left || token; right = right || next_token; if (right.id !== '(end)' && !option.white && (token.line !== right.line || token.thru + 1 !== right.from)) { warn('expected_space_a_b', right, artifact(token), artifact(right)); } } function one_space_only(left, right) { left = left || token; right = right || next_token; if (right.id !== '(end)' && (left.line !== right.line || (!option.white && left.thru + 1 !== right.from))) { warn('expected_space_a_b', right, artifact(left), artifact(right)); } } function no_space(left, right) { left = left || token; right = right || next_token; if ((!option.white || xmode === 'styleproperty' || xmode === 'style') && left.thru !== right.from && left.line === right.line) { warn('unexpected_space_a_b', right, artifact(left), artifact(right)); } } function no_space_only(left, right) { left = left || token; right = right || next_token; if (right.id !== '(end)' && (left.line !== right.line || (!option.white && left.thru !== right.from))) { warn('unexpected_space_a_b', right, artifact(left), artifact(right)); } } function spaces(left, right) { if (!option.white) { left = left || token; right = right || next_token; if (left.thru === right.from && left.line === right.line) { warn('missing_space_a_b', right, artifact(left), artifact(right)); } } } function comma() { if (next_token.id !== ',') { warn_at('expected_a_b', token.line, token.thru, ',', artifact()); } else { if (!option.white) { no_space_only(); } advance(','); spaces(); } } function semicolon() { if (next_token.id !== ';') { warn_at('expected_a_b', token.line, token.thru, ';', artifact()); } else { if (!option.white) { no_space_only(); } advance(';'); if (semicolon_coda[next_token.id] !== true) { spaces(); } } } function use_strict() { if (next_token.string === 'use strict') { if (strict_mode) { warn('unnecessary_use'); } edge(); advance(); semicolon(); strict_mode = true; option.newcap = false; option.undef = false; return true; } else { return false; } } function are_similar(a, b) { if (a === b) { return true; } if (Array.isArray(a)) { if (Array.isArray(b) && a.length === b.length) { var i; for (i = 0; i < a.length; i += 1) { if (!are_similar(a[i], b[i])) { return false; } } return true; } return false; } if (Array.isArray(b)) { return false; } if (a.id === '(number)' && b.id === '(number)') { return a.number === b.number; } if (a.arity === b.arity && a.string === b.string) { switch (a.arity) { case 'prefix': case 'suffix': case undefined: return a.id === b.id && are_similar(a.first, b.first); case 'infix': return are_similar(a.first, b.first) && are_similar(a.second, b.second); case 'ternary': return are_similar(a.first, b.first) && are_similar(a.second, b.second) && are_similar(a.third, b.third); case 'function': case 'regexp': return false; default: return true; } } else { if (a.id === '.' && b.id === '[' && b.arity === 'infix') { return a.second.string === b.second.string && b.second.id === '(string)'; } else if (a.id === '[' && a.arity === 'infix' && b.id === '.') { return a.second.string === b.second.string && a.second.id === '(string)'; } } return false; } // This is the heart of JSLINT, the Pratt parser. In addition to parsing, it // is looking for ad hoc lint patterns. We add .fud to Pratt's model, which is // like .nud except that it is only used on the first token of a statement. // Having .fud makes it much easier to define statement-oriented languages like // JavaScript. I retained Pratt's nomenclature. // .nud Null denotation // .fud First null denotation // .led Left denotation // lbp Left binding power // rbp Right binding power // They are elements of the parsing method called Top Down Operator Precedence. function expression(rbp, initial) { // rbp is the right binding power. // initial indicates that this is the first expression of a statement. var left; if (next_token.id === '(end)') { stop('unexpected_a', token, next_token.id); } advance(); if (option.safe && scope[token.string] && scope[token.string] === global_scope[token.string] && (next_token.id !== '(' && next_token.id !== '.')) { warn('adsafe_a', token); } if (initial) { anonname = 'anonymous'; funct['(verb)'] = token.string; } if (initial === true && token.fud) { left = token.fud(); } else { if (token.nud) { left = token.nud(); } else { if (next_token.id === '(number)' && token.id === '.') { warn('leading_decimal_a', token, artifact()); advance(); return token; } else { stop('expected_identifier_a', token, token.id); } } while (rbp < next_token.lbp) { advance(); if (token.led) { left = token.led(left); } else { stop('expected_operator_a', token, token.id); } } } return left; } // Functional constructors for making the symbols that will be inherited by // tokens. function symbol(s, p) { var x = syntax[s]; if (!x || typeof x !== 'object') { syntax[s] = x = { id: s, lbp: p || 0, string: s }; } return x; } function postscript(x) { x.postscript = true; return x; } function ultimate(s) { var x = symbol(s, 0); x.from = 1; x.thru = 1; x.line = 0; x.edge = 'edge'; s.string = s; return postscript(x); } function stmt(s, f) { var x = symbol(s); x.identifier = x.reserved = true; x.fud = f; return x; } function labeled_stmt(s, f) { var x = stmt(s, f); x.labeled = true; } function disrupt_stmt(s, f) { var x = stmt(s, f); x.disrupt = true; } function reserve_name(x) { var c = x.id.charAt(0); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { x.identifier = x.reserved = true; } return x; } function prefix(s, f, type) { var x = symbol(s, 150); reserve_name(x); x.nud = typeof f === 'function' ? f : function () { if (s === 'typeof') { one_space(); } else { no_space_only(); } this.first = expression(150); this.arity = 'prefix'; if (this.id === '++' || this.id === '--') { if (!option.plusplus) { warn('unexpected_a', this); } else if ((!this.first.identifier || this.first.reserved) && this.first.id !== '.' && this.first.id !== '[') { warn('bad_operand', this); } } this.type = type; return this; }; return x; } function type(s, t, nud) { var x = symbol(s); x.arity = x.type = t; if (nud) { x.nud = nud; } return x; } function reserve(s, f) { var x = symbol(s); x.identifier = x.reserved = true; if (typeof f === 'function') { x.nud = f; } return x; } function constant(name, type) { var x = reserve(name); x.type = type; x.string = name; x.nud = return_this; return x; } function reservevar(s, v) { return reserve(s, function () { if (typeof v === 'function') { v(this); } return this; }); } function infix(s, p, f, type, w) { var x = symbol(s, p); reserve_name(x); x.led = function (left) { this.arity = 'infix'; if (!w) { spaces(prev_token, token); spaces(); } if (!option.bitwise && this.bitwise) { warn('unexpected_a', this); } if (typeof f === 'function') { return f(left, this); } else { this.first = left; this.second = expression(p); return this; } }; if (type) { x.type = type; } return x; } function expected_relation(node, message) { if (node.assign) { warn(message || bundle.conditional_assignment, node); } return node; } function expected_condition(node, message) { switch (node.id) { case '[': case '-': if (node.arity !== 'infix') { warn(message || bundle.weird_condition, node); } break; case 'false': case 'function': case 'Infinity': case 'NaN': case 'null': case 'true': case 'undefined': case 'void': case '(number)': case '(regexp)': case '(string)': case '{': warn(message || bundle.weird_condition, node); break; case '(': if (node.first.id === '.' && numbery[node.first.second.string] === true) { warn(message || bundle.weird_condition, node); } break; } return node; } function check_relation(node) { switch (node.arity) { case 'prefix': switch (node.id) { case '{': case '[': warn('unexpected_a', node); break; case '!': warn('confusing_a', node); break; } break; case 'function': case 'regexp': warn('unexpected_a', node); break; default: if (node.id === 'NaN') { warn('isnan', node); } } return node; } function relation(s, eqeq) { return infix(s, 100, function (left, that) { check_relation(left); if (eqeq && !option.eqeq) { warn('expected_a_b', that, eqeq, that.id); } var right = expression(100); if (are_similar(left, right) || ((left.id === '(string)' || left.id === '(number)') && (right.id === '(string)' || right.id === '(number)'))) { warn('weird_relation', that); } that.first = left; that.second = check_relation(right); return that; }, 'boolean'); } function assignop(s, op) { var x = infix(s, 20, function (left, that) { var l; that.first = left; if (left.identifier) { if (scope[left.string]) { if (scope[left.string].writeable === false) { warn('read_only', left); } } else { stop('read_only'); } } else if (option.safe) { l = left; do { if (typeof predefined[l.string] === 'boolean') { warn('adsafe_a', l); } l = l.first; } while (l); } if (left === syntax['function']) { warn('identifier_function', token); } if (left.id === '.' || left.id === '[') { if (!left.first || left.first.string === 'arguments') { warn('bad_assignment', that); } } else if (left.identifier) { if (!left.reserved && funct[left.string] === 'exception') { warn('assign_exception', left); } } else { warn('bad_assignment', that); } that.second = expression(19); if (that.id === '=' && are_similar(that.first, that.second)) { warn('weird_assignment', that); } return that; }); x.assign = true; if (op) { if (syntax[op].type) { x.type = syntax[op].type; } if (syntax[op].bitwise) { x.bitwise = true; } } return x; } function bitwise(s, p) { var x = infix(s, p, 'number'); x.bitwise = true; return x; } function suffix(s) { var x = symbol(s, 150); x.led = function (left) { no_space_only(prev_token, token); if (!option.plusplus) { warn('unexpected_a', this); } else if ((!left.identifier || left.reserved) && left.id !== '.' && left.id !== '[') { warn('bad_operand', this); } this.first = left; this.arity = 'suffix'; return this; }; return x; } function optional_identifier() { if (next_token.identifier) { advance(); if (option.safe && banned[token.string]) { warn('adsafe_a', token); } else if (token.reserved && !option.es5) { warn('expected_identifier_a_reserved', token); } return token.string; } } function identifier() { var i = optional_identifier(); if (!i) { stop(token.id === 'function' && next_token.id === '(' ? 'name_function' : 'expected_identifier_a'); } return i; } function statement() { var label, old_scope = scope, the_statement; // We don't like the empty statement. if (next_token.id === ';') { warn('unexpected_a'); semicolon(); return; } // Is this a labeled statement? if (next_token.identifier && !next_token.reserved && peek().id === ':') { edge('label'); label = next_token; advance(); advance(':'); scope = Object.create(old_scope); add_label(label, 'label'); if (next_token.labeled !== true) { warn('label_a_b', next_token, label.string, artifact()); } else if (jx.test(label.string + ':')) { warn('url', label); } else if (funct === global_funct) { stop('unexpected_a', token); } next_token.label = label; } // Parse the statement. if (token.id !== 'else') { edge(); } step_in('statement'); the_statement = expression(0, true); if (the_statement) { // Look for the final semicolon. if (the_statement.arity === 'statement') { if (the_statement.id === 'switch' || (the_statement.block && the_statement.id !== 'do')) { spaces(); } else { semicolon(); } } else { // If this is an expression statement, determine if it is acceptable. // We do not like // new Blah(); // statments. If it is to be used at all, new should only be used to make // objects, not side effects. The expression statements we do like do // assignment or invocation or delete. if (the_statement.id === '(') { if (the_statement.first.id === 'new') { warn('bad_new'); } } else if (!the_statement.assign && the_statement.id !== 'delete' && the_statement.id !== '++' && the_statement.id !== '--') { warn('assignment_function_expression', token); } semicolon(); } } step_out(); scope = old_scope; return the_statement; } function statements() { var array = [], disruptor, the_statement; // A disrupt statement may not be followed by any other statement. // If the last statement is disrupt, then the sequence is disrupt. while (next_token.postscript !== true) { if (next_token.id === ';') { warn('unexpected_a', next_token); semicolon(); } else { if (next_token.string === 'use strict') { if ((!node_js && xmode !== 'script') || funct !== global_funct || array.length > 0) { warn('function_strict'); } use_strict(); } if (disruptor) { warn('unreachable_a_b', next_token, next_token.string, disruptor.string); disruptor = null; } the_statement = statement(); if (the_statement) { array.push(the_statement); if (the_statement.disrupt) { disruptor = the_statement; array.disrupt = true; } } } } return array; } function block(ordinary) { // array block is array sequence of statements wrapped in braces. // ordinary is false for function bodies and try blocks. // ordinary is true for if statements, while, etc. var array, curly = next_token, old_in_block = in_block, old_scope = scope, old_strict_mode = strict_mode; in_block = ordinary; scope = Object.create(scope); spaces(); if (next_token.id === '{') { advance('{'); step_in(); if (!ordinary && !use_strict() && !old_strict_mode && !option.sloppy && funct['(context)'] === global_funct) { warn('missing_use_strict'); } array = statements(); strict_mode = old_strict_mode; step_out('}', curly); } else if (!ordinary) { stop('expected_a_b', next_token, '{', artifact()); } else { warn('expected_a_b', next_token, '{', artifact()); array = [statement()]; array.disrupt = array[0].disrupt; } funct['(verb)'] = null; scope = old_scope; in_block = old_in_block; if (ordinary && array.length === 0) { warn('empty_block'); } return array; } function tally_property(name) { if (option.properties && typeof property_type[name] !== 'string') { warn('unexpected_property_a', token, name); } if (typeof member[name] === 'number') { member[name] += 1; } else { member[name] = 1; } } // ECMAScript parser syntax['(identifier)'] = { id: '(identifier)', lbp: 0, identifier: true, nud: function () { var name = this.string, variable = scope[name], site, writeable; // If the variable is not in scope, then we may have an undeclared variable. // Check the predefined list. If it was predefined, create the global // variable. if (typeof variable !== 'object') { writeable = predefined[name]; if (typeof writeable === 'boolean') { global_scope[name] = variable = { string: name, writeable: writeable, funct: global_funct }; global_funct[name] = 'var'; // But if the variable is not in scope, and is not predefined, and if we are not // in the global scope, then we have an undefined variable error. } else { if (!option.undef) { warn('used_before_a', token); } scope[name] = variable = { string: name, writeable: true, funct: funct }; funct[name] = 'undef'; } } site = variable.funct; // The name is in scope and defined in the current function. if (funct === site) { // Change 'unused' to 'var', and reject labels. switch (funct[name]) { case 'becoming': warn('unexpected_a', token); funct[name] = 'var'; break; case 'unused': funct[name] = 'var'; break; case 'unparam': funct[name] = 'parameter'; break; case 'unction': funct[name] = 'function'; break; case 'label': warn('a_label', token, name); break; } // If the name is already defined in the current // function, but not as outer, then there is a scope error. } else { switch (funct[name]) { case 'closure': case 'function': case 'var': case 'unused': warn('a_scope', token, name); break; case 'label': warn('a_label', token, name); break; case 'outer': case 'global': break; default: // If the name is defined in an outer function, make an outer entry, and if // it was unused, make it var. switch (site[name]) { case 'becoming': case 'closure': case 'function': case 'parameter': case 'unction': case 'unused': case 'var': site[name] = 'closure'; funct[name] = site === global_funct ? 'global' : 'outer'; break; case 'unparam': site[name] = 'parameter'; funct[name] = 'outer'; break; case 'undef': funct[name] = 'undef'; break; case 'label': warn('a_label', token, name); break; } } } return this; }, led: function () { stop('expected_operator_a'); } }; // Build the syntax table by declaring the syntactic elements. type('(array)', 'array'); type('(color)', 'color'); type('(function)', 'function'); type('(number)', 'number', return_this); type('(object)', 'object'); type('(string)', 'string', return_this); type('(boolean)', 'boolean', return_this); type('(range)', 'range'); type('(regexp)', 'regexp', return_this); ultimate('(begin)'); ultimate('(end)'); ultimate('(error)'); postscript(symbol('</')); symbol('<!'); symbol('<!--'); symbol('-->'); postscript(symbol('}')); symbol(')'); symbol(']'); postscript(symbol('"')); postscript(symbol('\'')); symbol(';'); symbol(':'); symbol(','); symbol('#'); symbol('@'); symbol('*/'); postscript(reserve('case')); reserve('catch'); postscript(reserve('default')); reserve('else'); reserve('finally'); reservevar('arguments', function (x) { if (strict_mode && funct === global_funct) { warn('strict', x); } else if (option.safe) { warn('adsafe_a', x); } }); reservevar('eval', function (x) { if (option.safe) { warn('adsafe_a', x); } }); constant('false', 'boolean'); constant('Infinity', 'number'); constant('NaN', 'number'); constant('null', ''); reservevar('this', function (x) { if (option.safe) { warn('adsafe_a', x); } else if (strict_mode && funct['(token)'].arity === 'statement' && funct['(name)'].charAt(0) > 'Z') { warn('strict', x); } }); constant('true', 'boolean'); constant('undefined', ''); infix('?', 30, function (left, that) { step_in('?'); that.first = expected_condition(expected_relation(left)); that.second = expression(0); spaces(); step_out(); var colon = next_token; advance(':'); step_in(':'); spaces(); that.third = expression(10); that.arity = 'ternary'; if (are_similar(that.second, that.third)) { warn('weird_ternary', colon); } else if (are_similar(that.first, that.second)) { warn('use_or', that); } step_out(); return that; }); infix('||', 40, function (left, that) { function paren_check(that) { if (that.id === '&&' && !that.paren) { warn('and', that); } return that; } that.first = paren_check(expected_condition(expected_relation(left))); that.second = paren_check(expected_relation(expression(40))); if (are_similar(that.first, that.second)) { warn('weird_condition', that); } return that; }); infix('&&', 50, function (left, that) { that.first = expected_condition(expected_relation(left)); that.second = expected_relation(expression(50)); if (are_similar(that.first, that.second)) { warn('weird_condition', that); } return that; }); prefix('void', function () { this.first = expression(0); this.arity = 'prefix'; if (option.es5) { warn('expected_a_b', this, 'undefined', 'void'); } else if (this.first.number !== 0) { warn('expected_a_b', this.first, '0', artifact(this.first)); } this.type = 'undefined'; return this; }); bitwise('|', 70); bitwise('^', 80); bitwise('&', 90); relation('==', '==='); relation('==='); relation('!=', '!=='); relation('!=='); relation('<'); relation('>'); relation('<='); relation('>='); bitwise('<<', 120); bitwise('>>', 120); bitwise('>>>', 120); infix('in', 120, function (left, that) { warn('infix_in', that); that.left = left; that.right = expression(130); return that; }, 'boolean'); infix('instanceof', 120, null, 'boolean'); infix('+', 130, function (left, that) { if (left.id === '(number)') { if (left.number === 0) { warn('unexpected_a', left, '0'); } } else if (left.id === '(string)') { if (left.string === '') { warn('expected_a_b', left, 'String', '\'\''); } } var right = expression(130); if (right.id === '(number)') { if (right.number === 0) { warn('unexpected_a', right, '0'); } } else if (right.id === '(string)') { if (right.string === '') { warn('expected_a_b', right, 'String', '\'\''); } } if (left.id === right.id) { if (left.id === '(string)' || left.id === '(number)') { if (left.id === '(string)') { left.string += right.string; if (jx.test(left.string)) { warn('url', left); } } else { left.number += right.number; } left.thru = right.thru; return left; } } that.first = left; that.second = right; return that; }); prefix('+', 'num'); prefix('+++', function () { warn('confusing_a', token); this.first = expression(150); this.arity = 'prefix'; return this; }); infix('+++', 130, function (left) { warn('confusing_a', token); this.first = left; this.second = expression(130); return this; }); infix('-', 130, function (left, that) { if ((left.id === '(number)' && left.number === 0) || left.id === '(string)') { warn('unexpected_a', left); } var right = expression(130); if ((right.id === '(number)' && right.number === 0) || right.id === '(string)') { warn('unexpected_a', left); } if (left.id === right.id && left.id === '(number)') { left.number -= right.number; left.thru = right.thru; return left; } that.first = left; that.second = right; return that; }, 'number'); prefix('-'); prefix('---', function () { warn('confusing_a', token); this.first = expression(150); this.arity = 'prefix'; return this; }); infix('---', 130, function (left) { warn('confusing_a', token); this.first = left; this.second = expression(130); return this; }); infix('*', 140, function (left, that) { if ((left.id === '(number)' && (left.number === 0 || left.number === 1)) || left.id === '(string)') { warn('unexpected_a', left); } var right = expression(140); if ((right.id === '(number)' && (right.number === 0 || right.number === 1)) || right.id === '(string)') { warn('unexpected_a', right); } if (left.id === right.id && left.id === '(number)') { left.number *= right.number; left.thru = right.thru; return left; } that.first = left; that.second = right; return that; }, 'number'); infix('/', 140, function (left, that) { if ((left.id === '(number)' && left.number === 0) || left.id === '(string)') { warn('unexpected_a', left); } var right = expression(140); if ((right.id === '(number)' && (right.number === 0 || right.number === 1)) || right.id === '(string)') { warn('unexpected_a', right); } if (left.id === right.id && left.id === '(number)') { left.number /= right.number; left.thru = right.thru; return left; } that.first = left; that.second = right; return that; }, 'number'); infix('%', 140, function (left, that) { if ((left.id === '(number)' && (left.number === 0 || left.number === 1)) || left.id === '(string)') { warn('unexpected_a', left); } var right = expression(140); if ((right.id === '(number)' && right.number === 0) || right.id === '(string)') { warn('unexpected_a', right); } if (left.id === right.id && left.id === '(number)') { left.number %= right.number; left.thru = right.thru; return left; } that.first = left; that.second = right; return that; }, 'number'); suffix('++'); prefix('++'); suffix('--'); prefix('--'); prefix('delete', function () { one_space(); var p = expression(0); if (!p || (p.id !== '.' && p.id !== '[')) { warn('deleted'); } this.first = p; return this; }); prefix('~', function () { no_space_only(); if (!option.bitwise) { warn('unexpected_a', this); } expression(150); return this; }, 'number'); prefix('!', function () { no_space_only(); this.first = expected_condition(expression(150)); this.arity = 'prefix'; if (bang[this.first.id] === true || this.first.assign) { warn('confusing_a', this); } return this; }, 'boolean'); prefix('typeof', null, 'string'); prefix('new', function () { one_space(); var c = expression(160), n, p, v; this.first = c; if (c.id !== 'function') { if (c.identifier) { switch (c.string) { case 'Object': warn('use_object', token); break; case 'Array': if (next_token.id === '(') { p = next_token; p.first = this; advance('('); if (next_token.id !== ')') { n = expression(0); p.second = [n]; if (n.type !== 'number' || next_token.id === ',') { warn('use_array', p); } while (next_token.id === ',') { advance(','); p.second.push(expression(0)); } } else { warn('use_array', token); } advance(')', p); return p; } warn('use_array', token); break; case 'Number': case 'String': case 'Boolean': case 'Math': case 'JSON': warn('not_a_constructor', c); break; case 'Function': if (!option.evil) { warn('function_eval'); } break; case 'Date': case 'RegExp': case 'this': break; default: if (c.id !== 'function') { v = c.string.charAt(0); if (!option.newcap && (v < 'A' || v > 'Z')) { warn('constructor_name_a', token); } } } } else { if (c.id !== '.' && c.id !== '[' && c.id !== '(') { warn('bad_constructor', token); } } } else { warn('weird_new', this); } if (next_token.id !== '(') { warn('missing_a', next_token, '()'); } return this; }); infix('(', 160, function (left, that) { var p; if (indent && indent.mode === 'expression') { no_space(prev_token, token); } else { no_space_only(prev_token, token); } if (!left.immed && left.id === 'function') { warn('wrap_immediate'); } p = []; if (left.identifier) { if (left.string.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) { if (left.string !== 'Number' && left.string !== 'String' && left.string !== 'Boolean' && left.string !== 'Date') { if (left.string === 'Math' || left.string === 'JSON') { warn('not_a_function', left); } else if (left.string === 'Object') { warn('use_object', token); } else if (left.string === 'Array' || !option.newcap) { warn('missing_a', left, 'new'); } } } } else if (left.id === '.') { if (option.safe && left.first.string === 'Math' && left.second === 'random') { warn('adsafe_a', left); } else if (left.second.string === 'split' && left.first.id === '(string)') { warn('use_array', left.second); } } step_in(); if (next_token.id !== ')') { no_space(); for (;;) { edge(); p.push(expression(10)); if (next_token.id !== ',') { break; } comma(); } } no_space(); step_out(')', that); if (typeof left === 'object') { if (left.string === 'parseInt' && p.length === 1) { warn('radix', left); } if (!option.evil) { if (left.string === 'eval' || left.string === 'Function' || left.string === 'execScript') { warn('evil', left); } else if (p[0] && p[0].id === '(string)' && (left.string === 'setTimeout' || left.string === 'setInterval')) { warn('implied_evil', left); } } if (!left.identifier && left.id !== '.' && left.id !== '[' && left.id !== '(' && left.id !== '&&' && left.id !== '||' && left.id !== '?') { warn('bad_invocation', left); } } that.first = left; that.second = p; return that; }, '', true); prefix('(', function () { step_in('expression'); no_space(); edge(); if (next_token.id === 'function') { next_token.immed = true; } var value = expression(0); value.paren = true; no_space(); step_out(')', this); if (value.id === 'function') { if (next_token.id === '(') { warn('move_invocation'); } else { warn('bad_wrap', this); } } return value; }); infix('.', 170, function (left, that) { no_space(prev_token, token); no_space(); var name = identifier(), type; if (typeof name === 'string') { tally_property(name); } that.first = left; that.second = token; if (left && left.string === 'arguments' && (name === 'callee' || name === 'caller')) { warn('avoid_a', left, 'arguments.' + name); } else if (!option.evil && left && left.string === 'document' && (name === 'write' || name === 'writeln')) { warn('write_is_wrong', left); } else if (option.adsafe) { if (!adsafe_top && left.string === 'ADSAFE') { if (name === 'id' || name === 'lib') { warn('adsafe_a', that); } else if (name === 'go') { if (xmode !== 'script') { warn('adsafe_a', that); } else if (adsafe_went || next_token.id !== '(' || peek(0).id !== '(string)' || peek(0).string !== adsafe_id || peek(1).id !== ',') { stop('adsafe_a', that, 'go'); } adsafe_went = true; adsafe_may = false; } } adsafe_top = false; } if (!option.evil && (name === 'eval' || name === 'execScript')) { warn('evil'); } else if (option.safe) { for (;;) { if (banned[name] === true) { warn('adsafe_a', token, name); } if (typeof predefined[left.string] !== 'boolean' || //// check for writeable next_token.id === '(') { break; } if (next_token.id !== '.') { warn('adsafe_a', that); break; } advance('.'); token.first = that; token.second = name; that = token; name = identifier(); if (typeof name === 'string') { tally_property(name); } } } type = property_type[name]; if (type && typeof type === 'string' && type !== '*') { that.type = type; } return that; }, '', true); infix('[', 170, function (left, that) { var e, s; no_space_only(prev_token, token); no_space(); step_in(); edge(); e = expression(0); switch (e.type) { case 'number': if (e.id === '(number)' && left.id === 'arguments') { warn('use_param', left); } break; case 'string': if (e.id === '(string)') { if (option.safe && (banned[e.string] || e.string.charAt(0) === '_' || e.string.slice(-1) === '_')) { warn('adsafe_subscript_a', e); } else if (!option.evil && (e.string === 'eval' || e.string === 'execScript')) { warn('evil', e); } else if (!option.sub && ix.test(e.string)) { s = syntax[e.string]; if (!s || !s.reserved) { warn('subscript', e); } } tally_property(e.string); } else if (option.safe && e.id !== 'typeof') { warn('adsafe_subscript_a', e); } break; case undefined: if (option.safe) { warn('adsafe_subscript_a', e); } break; default: if (option.safe) { warn('adsafe_subscript_a', e); } } step_out(']', that); no_space(prev_token, token); that.first = left; that.second = e; return that; }, '', true); prefix('[', function () { this.arity = 'prefix'; this.first = []; step_in('array'); while (next_token.id !== '(end)') { while (next_token.id === ',') { warn('unexpected_a', next_token); advance(','); } if (next_token.id === ']') { break; } indent.wrap = false; edge(); this.first.push(expression(10)); if (next_token.id === ',') { comma(); if (next_token.id === ']' && !option.es5) { warn('unexpected_a', token); break; } } else { break; } } step_out(']', this); return this; }, 170); function property_name() { var id = optional_identifier(true); if (!id) { if (next_token.id === '(string)') { id = next_token.string; if (option.safe) { if (banned[id]) { warn('adsafe_a'); } else if (id.charAt(0) === '_' || id.charAt(id.length - 1) === '_') { warn('dangling_a'); } } advance(); } else if (next_token.id === '(number)') { id = next_token.number.toString(); advance(); } } return id; } function function_params() { var id, paren = next_token, params = []; advance('('); step_in(); no_space(); if (next_token.id === ')') { no_space(); step_out(')', paren); return; } for (;;) { edge(); id = identifier(); params.push(token); add_label(token, option.unparam ? 'parameter' : 'unparam'); if (next_token.id === ',') { comma(); } else { no_space(); step_out(')', paren); return params; } } } function do_function(func, name) { var old_funct = funct, old_option = option, old_scope = scope; funct = { '(name)' : name || '\'' + (anonname || '').replace(nx, sanitize) + '\'', '(line)' : next_token.line, '(context)' : old_funct, '(breakage)' : 0, '(loopage)' : 0, '(scope)' : scope, '(token)' : func }; option = Object.create(old_option); scope = Object.create(old_scope); functions.push(funct); func.name = name; if (name) { add_label(func, 'function', name); } func.writeable = false; func.first = funct['(params)'] = function_params(); one_space(); func.block = block(false); if (funct['(old_property_type)']) { property_type = funct['(old_property_type)']; delete funct['(old_property_type)']; } if (option.confusion) { funct['(confusion)'] = true; } funct = old_funct; option = old_option; scope = old_scope; } assignop('='); assignop('+=', '+'); assignop('-=', '-'); assignop('*=', '*'); assignop('/=', '/').nud = function () { stop('slash_equal'); }; assignop('%=', '%'); assignop('&=', '&'); assignop('|=', '|'); assignop('^=', '^'); assignop('<<=', '<<'); assignop('>>=', '>>'); assignop('>>>=', '>>>'); prefix('{', function () { var get, i, j, name, p, set, seen = {}; this.arity = 'prefix'; this.first = []; step_in(); while (next_token.id !== '}') { indent.wrap = false; // JSLint recognizes the ES5 extension for get/set in object literals, // but requires that they be used in pairs. edge(); if (next_token.string === 'get' && peek().id !== ':') { if (!option.es5) { warn('es5'); } get = next_token; advance('get'); one_space_only(); name = next_token; i = property_name(); if (!i) { stop('missing_property'); } get.string = ''; do_function(get); if (funct['(loopage)']) { warn('function_loop', get); } p = get.first; if (p) { warn('parameter_a_get_b', p[0], p[0].string, i); } comma(); set = next_token; spaces(); edge(); advance('set'); set.string = ''; one_space_only(); j = property_name(); if (i !== j) { stop('expected_a_b', token, i, j || next_token.string); } do_function(set); if (set.block.length === 0) { warn('missing_a', token, 'throw'); } p = set.first; if (!p || p.length !== 1) { stop('parameter_set_a', set, 'value'); } else if (p[0].string !== 'value') { stop('expected_a_b', p[0], 'value', p[0].string); } name.first = [get, set]; } else { name = next_token; i = property_name(); if (typeof i !== 'string') { stop('missing_property'); } advance(':'); spaces(); name.first = expression(10); } this.first.push(name); if (seen[i] === true) { warn('duplicate_a', next_token, i); } seen[i] = true; tally_property(i); if (next_token.id !== ',') { break; } for (;;) { comma(); if (next_token.id !== ',') { break; } warn('unexpected_a', next_token); } if (next_token.id === '}' && !option.es5) { warn('unexpected_a', token); } } step_out('}', this); return this; }); stmt('{', function () { warn('statement_block'); this.arity = 'statement'; this.block = statements(); this.disrupt = this.block.disrupt; advance('}', this); return this; }); stmt('/*global', directive); stmt('/*globals', directive); stmt('/*jslint', directive); stmt('/*member', directive); stmt('/*members', directive); stmt('/*property', directive); stmt('/*properties', directive); stmt('var', function () { // JavaScript does not have block scope. It only has function scope. So, // declaring a variable in a block can have unexpected consequences. // var.first will contain an array, the array containing name tokens // and assignment tokens. var assign, id, name; if (funct['(vars)'] && !option.vars) { warn('combine_var'); } else if (funct !== global_funct) { funct['(vars)'] = true; } this.arity = 'statement'; this.first = []; step_in('var'); for (;;) { name = next_token; id = identifier(); add_label(name, 'becoming'); if (next_token.id === '=') { assign = next_token; assign.first = name; spaces(); advance('='); spaces(); if (next_token.id === 'undefined') { warn('unnecessary_initialize', token, id); } if (peek(0).id === '=' && next_token.identifier) { stop('var_a_not'); } assign.second = expression(0); assign.arity = 'infix'; this.first.push(assign); } else { this.first.push(name); } if (funct[id] === 'becoming') { funct[id] = 'unused'; } if (next_token.id !== ',') { break; } comma(); indent.wrap = false; if (var_mode && next_token.line === token.line && this.first.length === 1) { var_mode = null; indent.open = false; indent.at -= option.indent; } spaces(); edge(); } var_mode = null; step_out(); return this; }); stmt('function', function () { one_space(); if (in_block) { warn('function_block', token); } var name = next_token, id = identifier(); add_label(name, 'unction'); no_space(); this.arity = 'statement'; do_function(this, id); if (next_token.id === '(' && next_token.line === token.line) { stop('function_statement'); } return this; }); prefix('function', function () { if (!option.anon) { one_space(); } var id = optional_identifier(); if (id) { no_space(); } else { id = ''; } do_function(this, id); if (funct['(loopage)']) { warn('function_loop'); } this.arity = 'function'; return this; }); stmt('if', function () { var paren = next_token; one_space(); advance('('); step_in('control'); no_space(); edge(); this.arity = 'statement'; this.first = expected_condition(expected_relation(expression(0))); no_space(); step_out(')', paren); one_space(); this.block = block(true); if (next_token.id === 'else') { one_space(); advance('else'); one_space(); this['else'] = next_token.id === 'if' || next_token.id === 'switch' ? statement(true) : block(true); if (this['else'].disrupt && this.block.disrupt) { this.disrupt = true; } } return this; }); stmt('try', function () { // try.first The catch variable // try.second The catch clause // try.third The finally clause // try.block The try block var exception_variable, old_scope, paren; if (option.adsafe) { warn('adsafe_a', this); } one_space(); this.arity = 'statement'; this.block = block(false); if (next_token.id === 'catch') { one_space(); advance('catch'); one_space(); paren = next_token; advance('('); step_in('control'); no_space(); edge(); old_scope = scope; scope = Object.create(old_scope); exception_variable = next_token.string; this.first = exception_variable; if (!next_token.identifier) { warn('expected_identifier_a', next_token); } else { add_label(next_token, 'exception'); } advance(); no_space(); step_out(')', paren); one_space(); this.second = block(false); scope = old_scope; } if (next_token.id === 'finally') { one_space(); advance('finally'); one_space(); this.third = block(false); } else if (!this.second) { stop('expected_a_b', next_token, 'catch', artifact()); } return this; }); labeled_stmt('while', function () { one_space(); var paren = next_token; funct['(breakage)'] += 1; funct['(loopage)'] += 1; advance('('); step_in('control'); no_space(); edge(); this.arity = 'statement'; this.first = expected_relation(expression(0)); if (this.first.id !== 'true') { expected_condition(this.first, bundle.unexpected_a); } no_space(); step_out(')', paren); one_space(); this.block = block(true); if (this.block.disrupt) { warn('strange_loop', prev_token); } funct['(breakage)'] -= 1; funct['(loopage)'] -= 1; return this; }); reserve('with'); labeled_stmt('switch', function () { // switch.first the switch expression // switch.second the array of cases. A case is 'case' or 'default' token: // case.first the array of case expressions // case.second the array of statements // If all of the arrays of statements are disrupt, then the switch is disrupt. var cases = [], old_in_block = in_block, particular, the_case = next_token, unbroken = true; function find_duplicate_case(value) { if (are_similar(particular, value)) { warn('duplicate_a', value); } } funct['(breakage)'] += 1; one_space(); advance('('); no_space(); step_in(); this.arity = 'statement'; this.first = expected_condition(expected_relation(expression(0))); no_space(); step_out(')', the_case); one_space(); advance('{'); step_in(); in_block = true; this.second = []; while (next_token.id === 'case') { the_case = next_token; cases.forEach(find_duplicate_case); the_case.first = []; the_case.arity = 'case'; spaces(); edge('case'); advance('case'); for (;;) { one_space(); particular = expression(0); cases.forEach(find_duplicate_case); cases.push(particular); the_case.first.push(particular); if (particular.id === 'NaN') { warn('unexpected_a', particular); } no_space_only(); advance(':'); if (next_token.id !== 'case') { break; } spaces(); edge('case'); advance('case'); } spaces(); the_case.second = statements(); if (the_case.second && the_case.second.length > 0) { particular = the_case.second[the_case.second.length - 1]; if (particular.disrupt) { if (particular.id === 'break') { unbroken = false; } } else { warn('missing_a_after_b', next_token, 'break', 'case'); } } else { warn('empty_case'); } this.second.push(the_case); } if (this.second.length === 0) { warn('missing_a', next_token, 'case'); } if (next_token.id === 'default') { spaces(); the_case = next_token; the_case.arity = 'case'; edge('case'); advance('default'); no_space_only(); advance(':'); spaces(); the_case.second = statements(); if (the_case.second && the_case.second.length > 0) { particular = the_case.second[the_case.second.length - 1]; if (unbroken && particular.disrupt && particular.id !== 'break') { this.disrupt = true; } } this.second.push(the_case); } funct['(breakage)'] -= 1; spaces(); step_out('}', this); in_block = old_in_block; return this; }); stmt('debugger', function () { if (!option.debug) { warn('unexpected_a', this); } this.arity = 'statement'; return this; }); labeled_stmt('do', function () { funct['(breakage)'] += 1; funct['(loopage)'] += 1; one_space(); this.arity = 'statement'; this.block = block(true); if (this.block.disrupt) { warn('strange_loop', prev_token); } one_space(); advance('while'); var paren = next_token; one_space(); advance('('); step_in(); no_space(); edge(); this.first = expected_condition(expected_relation(expression(0)), bundle.unexpected_a); no_space(); step_out(')', paren); funct['(breakage)'] -= 1; funct['(loopage)'] -= 1; return this; }); labeled_stmt('for', function () { var blok, filter, ok = false, paren = next_token, value; this.arity = 'statement'; funct['(breakage)'] += 1; funct['(loopage)'] += 1; advance('('); if (next_token.id === ';') { no_space(); advance(';'); no_space(); advance(';'); no_space(); advance(')'); blok = block(true); } else { step_in('control'); spaces(this, paren); no_space(); if (next_token.id === 'var') { stop('move_var'); } edge(); if (peek(0).id === 'in') { this.forin = true; value = next_token; switch (funct[value.string]) { case 'unused': funct[value.string] = 'var'; break; case 'closure': case 'var': break; default: warn('bad_in_a', value); } advance(); advance('in'); this.first = value; this.second = expression(20); step_out(')', paren); blok = block(true); if (!option.forin) { if (blok.length === 1 && typeof blok[0] === 'object' && blok[0].string === 'if' && !blok[0]['else']) { filter = blok[0].first; while (filter.id === '&&') { filter = filter.first; } switch (filter.id) { case '===': case '!==': ok = filter.first.id === '[' ? filter.first.first.string === this.second.string && filter.first.second.string === this.first.string : filter.first.id === 'typeof' && filter.first.first.id === '[' && filter.first.first.first.string === this.second.string && filter.first.first.second.string === this.first.string; break; case '(': ok = filter.first.id === '.' && (( filter.first.first.string === this.second.string && filter.first.second.string === 'hasOwnProperty' && filter.second[0].string === this.first.string ) || ( filter.first.first.string === 'ADSAFE' && filter.first.second.string === 'has' && filter.second[0].string === this.second.string && filter.second[1].string === this.first.string ) || ( filter.first.first.id === '.' && filter.first.first.first.id === '.' && filter.first.first.first.first.string === 'Object' && filter.first.first.first.second.string === 'prototype' && filter.first.first.second.string === 'hasOwnProperty' && filter.first.second.string === 'call' && filter.second[0].string === this.second.string && filter.second[1].string === this.first.string )); break; } } if (!ok) { warn('for_if', this); } } } else { edge(); this.first = []; for (;;) { this.first.push(expression(0, 'for')); if (next_token.id !== ',') { break; } comma(); } semicolon(); edge(); this.second = expected_relation(expression(0)); if (this.second.id !== 'true') { expected_condition(this.second, bundle.unexpected_a); } semicolon(token); if (next_token.id === ';') { stop('expected_a_b', next_token, ')', ';'); } this.third = []; edge(); for (;;) { this.third.push(expression(0, 'for')); if (next_token.id !== ',') { break; } comma(); } no_space(); step_out(')', paren); one_space(); blok = block(true); } } if (blok.disrupt) { warn('strange_loop', prev_token); } this.block = blok; funct['(breakage)'] -= 1; funct['(loopage)'] -= 1; return this; }); disrupt_stmt('break', function () { var label = next_token.string; this.arity = 'statement'; if (funct['(breakage)'] === 0) { warn('unexpected_a', this); } if (next_token.identifier && token.line === next_token.line) { one_space_only(); if (funct[label] !== 'label') { warn('not_a_label', next_token); } else if (scope[label].funct !== funct) { warn('not_a_scope', next_token); } this.first = next_token; advance(); } return this; }); disrupt_stmt('continue', function () { if (!option['continue']) { warn('unexpected_a', this); } var label = next_token.string; this.arity = 'statement'; if (funct['(breakage)'] === 0) { warn('unexpected_a', this); } if (next_token.identifier && token.line === next_token.line) { one_space_only(); if (funct[label] !== 'label') { warn('not_a_label', next_token); } else if (scope[label].funct !== funct) { warn('not_a_scope', next_token); } this.first = next_token; advance(); } return this; }); disrupt_stmt('return', function () { if (funct === global_funct) { warn('unexpected_a', this); } this.arity = 'statement'; if (next_token.id !== ';' && next_token.line === token.line) { one_space_only(); if (next_token.id === '/' || next_token.id === '(regexp)') { warn('wrap_regexp'); } this.first = expression(20); } return this; }); disrupt_stmt('throw', function () { this.arity = 'statement'; one_space_only(); this.first = expression(20); return this; }); // Superfluous reserved words reserve('class'); reserve('const'); reserve('enum'); reserve('export'); reserve('extends'); reserve('import'); reserve('super'); // Harmony reserved words reserve('implements'); reserve('interface'); reserve('let'); reserve('package'); reserve('private'); reserve('protected'); reserve('public'); reserve('static'); reserve('yield'); // Type inference // function get_type(one) { // var type; // if (typeof one === 'string') { // return one; // } else if (one.type) { // return one.type; // } else if (one.id === '.') { // type = property_type[one.second.string]; // return typeof type === 'string' ? type : ''; // } else { // return ((one.identifier && scope[one.string]) || one).type; // } // } // function match_type(one_type, two_type, one, two) { // if (one_type === two_type) { // return true; // } else { // if (!funct.confusion && !two.warn) { // if (typeof one !== 'string') { // if (one.id === '.') { // one_type = '.' + one.second.string + ': ' + one_type; // } else { // one_type = one.string + ': ' + one_type; // } // } // if (two.id === '.') { // two_type = '.' + two.second.string + ': ' + one_type; // } else { // two_type = two.string + ': ' + one_type; // } // warn('type_confusion_a_b', two, one_type, two_type); // two.warn = true; // } // return false; // } // } // function conform(one, two) { // // // The conform function takes a type string and a token, or two tokens. // // var one_type = typeof one === 'string' ? one : one.type, // two_type = two.type, // two_thing; // // // If both tokens already have a type, and if they match, then we are done. // // Once a token has a type, it is locked. Neither token will change, but if // // they do not match, there will be a warning. // // if (one_type) { // if (two_type) { // match_type(one_type, two_type, one, two); // } else { // // // two does not have a type, so look deeper. If two is a variable or property, // // then use its type if it has one, and make the deep type one's type if it // // doesn't. If the type was *, or if there was a mismatch, don't change the // // deep type. // // two_thing = two.id === '(identifier)' // ? scope[two.string] // : two.id === '.' // ? property_type[two.second.string] // : null; // if (two_thing) { // two_type = two_thing.type; // if (two_type) { // if (two_type !== '*') { // if (!match_type(one_type, two_type, one, two)) { // return ''; // } // } // } else { // two_thing.type = one_type; // } // } // // // In any case, we give two a type. // // two.type = one_type; // type_state_change = true; // return one_type; // } // // // one does not have a type, but two does, so do the old switcheroo. // // } else { // if (two_type) { // return conform(two, one); // // // Neither token has a type yet. So we have to look deeper to see if either // // is a variable or property. // // } else { // if (one.id === '(identifier)') { // one_type = scope[one.string].type; // if (one_type && one_type !== '*') { // one.type = one_type; // return conform(one, two); // } // } else if (one.id === '.') { // one_type = property_type[one.second.string]; // if (one_type && one_type !== '*') { // one.type = scope[one.string].type; // return conform(one, two); // } // } // if (two.id === '(identifier)') { // two_type = scope[two.string].type; // if (two_type && two_type !== '*') { // two.type = two_type; // return conform(two, one); // } // } else if (two.id === '.') { // two_type = property_type[two.second.string]; // if (two_type && two_type !== '*') { // two.type = scope[two.string].type; // return conform(two, one); // } // } // } // } // // // Return a falsy string if we were unable to determine the type of either token. // // return ''; // } // function conform_array(type, array) { // array.forEach(function (item) { // return conform(type, item); // }, type); // } // function infer(node) { // if (Array.isArray(node)) { // node.forEach(infer); // } else { // switch (node.arity) { // case 'statement': // infer_statement[node.id](node); // break; // case 'infix': // infer(node.first); // infer(node.second); // switch (node.id) { // case '(': // conform('function', node.first); // break; // default: // stop('unfinished'); // } // break; // case 'number': // case 'string': // case 'boolean': // break; // default: // stop('unfinished'); // } // } // } // infer_statement = { // 'var': function (node) { // var i, item, list = node.first; // for (i = 0; i < list.length; i += 1) { // item = list[i]; // if (item.id === '=') { // infer(item.second); // conform(item.first, item.second); // conform(item.first, item); // } // } // }, // 'for': function (node) { // infer(node.first); // infer(node.second); // if (node.forin) { // conform('string', node.first); // conform('object', node.second); // } else { // infer(node.third); // conform_array('number', node.first); // conform('boolean', node.second); // conform_array('number', node.third); // } // infer(node.block); // } // }; // function infer_types(node) { // do { // funct = global_funct; // scope = global_scope; // type_state_change = false; // infer(node); // } while (type_state_change); // } // Parse JSON function json_value() { function json_object() { var brace = next_token, object = {}; advance('{'); if (next_token.id !== '}') { while (next_token.id !== '(end)') { while (next_token.id === ',') { warn('unexpected_a', next_token); advance(','); } if (next_token.id !== '(string)') { warn('expected_string_a'); } if (object[next_token.string] === true) { warn('duplicate_a'); } else if (next_token.string === '__proto__') { warn('dangling_a'); } else { object[next_token.string] = true; } advance(); advance(':'); json_value(); if (next_token.id !== ',') { break; } advance(','); if (next_token.id === '}') { warn('unexpected_a', token); break; } } } advance('}', brace); } function json_array() { var bracket = next_token; advance('['); if (next_token.id !== ']') { while (next_token.id !== '(end)') { while (next_token.id === ',') { warn('unexpected_a', next_token); advance(','); } json_value(); if (next_token.id !== ',') { break; } advance(','); if (next_token.id === ']') { warn('unexpected_a', token); break; } } } advance(']', bracket); } switch (next_token.id) { case '{': json_object(); break; case '[': json_array(); break; case 'true': case 'false': case 'null': case '(number)': case '(string)': advance(); break; case '-': advance('-'); no_space_only(); advance('(number)'); break; default: stop('unexpected_a'); } } // CSS parsing. function css_name() { if (next_token.identifier) { advance(); return true; } } function css_number() { if (next_token.id === '-') { advance('-'); no_space_only(); } if (next_token.id === '(number)') { advance('(number)'); return true; } } function css_string() { if (next_token.id === '(string)') { advance(); return true; } } function css_color() { var i, number, paren, value; if (next_token.identifier) { value = next_token.string; if (value === 'rgb' || value === 'rgba') { advance(); paren = next_token; advance('('); for (i = 0; i < 3; i += 1) { if (i) { comma(); } number = next_token.number; if (next_token.id !== '(number)' || number < 0) { warn('expected_positive_a', next_token); advance(); } else { advance(); if (next_token.id === '%') { advance('%'); if (number > 100) { warn('expected_percent_a', token, number); } } else { if (number > 255) { warn('expected_small_a', token, number); } } } } if (value === 'rgba') { comma(); number = next_token.number; if (next_token.id !== '(number)' || number < 0 || number > 1) { warn('expected_fraction_a', next_token); } advance(); if (next_token.id === '%') { warn('unexpected_a'); advance('%'); } } advance(')', paren); return true; } else if (css_colorData[next_token.string] === true) { advance(); return true; } } else if (next_token.id === '(color)') { advance(); return true; } return false; } function css_length() { if (next_token.id === '-') { advance('-'); no_space_only(); } if (next_token.id === '(number)') { advance(); if (next_token.id !== '(string)' && css_lengthData[next_token.string] === true) { no_space_only(); advance(); } else if (+token.number !== 0) { warn('expected_linear_a'); } return true; } return false; } function css_line_height() { if (next_token.id === '-') { advance('-'); no_space_only(); } if (next_token.id === '(number)') { advance(); if (next_token.id !== '(string)' && css_lengthData[next_token.string] === true) { no_space_only(); advance(); } return true; } return false; } function css_width() { if (next_token.identifier) { switch (next_token.string) { case 'thin': case 'medium': case 'thick': advance(); return true; } } else { return css_length(); } } function css_margin() { if (next_token.identifier) { if (next_token.string === 'auto') { advance(); return true; } } else { return css_length(); } } function css_attr() { if (next_token.identifier && next_token.string === 'attr') { advance(); advance('('); if (!next_token.identifier) { warn('expected_name_a'); } advance(); advance(')'); return true; } return false; } function css_comma_list() { while (next_token.id !== ';') { if (!css_name() && !css_string()) { warn('expected_name_a'); } if (next_token.id !== ',') { return true; } comma(); } } function css_counter() { if (next_token.identifier && next_token.string === 'counter') { advance(); advance('('); advance(); if (next_token.id === ',') { comma(); if (next_token.id !== '(string)') { warn('expected_string_a'); } advance(); } advance(')'); return true; } if (next_token.identifier && next_token.string === 'counters') { advance(); advance('('); if (!next_token.identifier) { warn('expected_name_a'); } advance(); if (next_token.id === ',') { comma(); if (next_token.id !== '(string)') { warn('expected_string_a'); } advance(); } if (next_token.id === ',') { comma(); if (next_token.id !== '(string)') { warn('expected_string_a'); } advance(); } advance(')'); return true; } return false; } function css_radius() { return css_length() && (next_token.id !== '(number)' || css_length()); } function css_shape() { var i; if (next_token.identifier && next_token.string === 'rect') { advance(); advance('('); for (i = 0; i < 4; i += 1) { if (!css_length()) { warn('expected_number_a'); break; } } advance(')'); return true; } return false; } function css_url() { var c, url; if (next_token.identifier && next_token.string === 'url') { next_token = lex.range('(', ')'); url = next_token.string; c = url.charAt(0); if (c === '"' || c === '\'') { if (url.slice(-1) !== c) { warn('bad_url_a'); } else { url = url.slice(1, -1); if (url.indexOf(c) >= 0) { warn('bad_url_a'); } } } if (!url) { warn('missing_url'); } if (ux.test(url)) { stop('bad_url_a'); } urls.push(url); advance(); return true; } return false; } css_any = [css_url, function () { for (;;) { if (next_token.identifier) { switch (next_token.string.toLowerCase()) { case 'url': css_url(); break; case 'expression': warn('unexpected_a'); advance(); break; default: advance(); } } else { if (next_token.id === ';' || next_token.id === '!' || next_token.id === '(end)' || next_token.id === '}') { return true; } advance(); } } }]; function font_face() { advance_identifier('font-family'); advance(':'); if (!css_name() && !css_string()) { stop('expected_name_a'); } semicolon(); advance_identifier('src'); advance(':'); while (true) { if (next_token.string === 'local') { advance_identifier('local'); advance('('); if (ux.test(next_token.string)) { stop('bad_url_a'); } if (!css_name() && !css_string()) { stop('expected_name_a'); } advance(')'); } else if (!css_url()) { stop('expected_a_b', next_token, 'url', artifact()); } if (next_token.id !== ',') { break; } comma(); } semicolon(); } css_border_style = [ 'none', 'dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'outset', 'ridge', 'solid' ]; css_break = [ 'auto', 'always', 'avoid', 'left', 'right' ]; css_media = { 'all': true, 'braille': true, 'embossed': true, 'handheld': true, 'print': true, 'projection': true, 'screen': true, 'speech': true, 'tty': true, 'tv': true }; css_overflow = [ 'auto', 'hidden', 'scroll', 'visible' ]; css_attribute_data = { background: [ true, 'background-attachment', 'background-color', 'background-image', 'background-position', 'background-repeat' ], 'background-attachment': ['scroll', 'fixed'], 'background-color': ['transparent', css_color], 'background-image': ['none', css_url], 'background-position': [ 2, [css_length, 'top', 'bottom', 'left', 'right', 'center'] ], 'background-repeat': [ 'repeat', 'repeat-x', 'repeat-y', 'no-repeat' ], 'border': [true, 'border-color', 'border-style', 'border-width'], 'border-bottom': [ true, 'border-bottom-color', 'border-bottom-style', 'border-bottom-width' ], 'border-bottom-color': css_color, 'border-bottom-left-radius': css_radius, 'border-bottom-right-radius': css_radius, 'border-bottom-style': css_border_style, 'border-bottom-width': css_width, 'border-collapse': ['collapse', 'separate'], 'border-color': ['transparent', 4, css_color], 'border-left': [ true, 'border-left-color', 'border-left-style', 'border-left-width' ], 'border-left-color': css_color, 'border-left-style': css_border_style, 'border-left-width': css_width, 'border-radius': function () { function count(separator) { var n = 1; if (separator) { advance(separator); } if (!css_length()) { return false; } while (next_token.id === '(number)') { if (!css_length()) { return false; } n += 1; } if (n > 4) { warn('bad_style'); } return true; } return count() && (next_token.id !== '/' || count('/')); }, 'border-right': [ true, 'border-right-color', 'border-right-style', 'border-right-width' ], 'border-right-color': css_color, 'border-right-style': css_border_style, 'border-right-width': css_width, 'border-spacing': [2, css_length], 'border-style': [4, css_border_style], 'border-top': [ true, 'border-top-color', 'border-top-style', 'border-top-width' ], 'border-top-color': css_color, 'border-top-left-radius': css_radius, 'border-top-right-radius': css_radius, 'border-top-style': css_border_style, 'border-top-width': css_width, 'border-width': [4, css_width], bottom: [css_length, 'auto'], 'caption-side' : ['bottom', 'left', 'right', 'top'], clear: ['both', 'left', 'none', 'right'], clip: [css_shape, 'auto'], color: css_color, content: [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote', css_string, css_url, css_counter, css_attr ], 'counter-increment': [ css_name, 'none' ], 'counter-reset': [ css_name, 'none' ], cursor: [ css_url, 'auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize', 'pointer', 's-resize', 'se-resize', 'sw-resize', 'w-resize', 'text', 'wait' ], direction: ['ltr', 'rtl'], display: [ 'block', 'compact', 'inline', 'inline-block', 'inline-table', 'list-item', 'marker', 'none', 'run-in', 'table', 'table-caption', 'table-cell', 'table-column', 'table-column-group', 'table-footer-group', 'table-header-group', 'table-row', 'table-row-group' ], 'empty-cells': ['show', 'hide'], 'float': ['left', 'none', 'right'], font: [ 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar', true, 'font-size', 'font-style', 'font-weight', 'font-family' ], 'font-family': css_comma_list, 'font-size': [ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller', css_length ], 'font-size-adjust': ['none', css_number], 'font-stretch': [ 'normal', 'wider', 'narrower', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded' ], 'font-style': [ 'normal', 'italic', 'oblique' ], 'font-variant': [ 'normal', 'small-caps' ], 'font-weight': [ 'normal', 'bold', 'bolder', 'lighter', css_number ], height: [css_length, 'auto'], left: [css_length, 'auto'], 'letter-spacing': ['normal', css_length], 'line-height': ['normal', css_line_height], 'list-style': [ true, 'list-style-image', 'list-style-position', 'list-style-type' ], 'list-style-image': ['none', css_url], 'list-style-position': ['inside', 'outside'], 'list-style-type': [ 'circle', 'disc', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'katakana', 'hiragana-iroha', 'katakana-oroha', 'none' ], margin: [4, css_margin], 'margin-bottom': css_margin, 'margin-left': css_margin, 'margin-right': css_margin, 'margin-top': css_margin, 'marker-offset': [css_length, 'auto'], 'max-height': [css_length, 'none'], 'max-width': [css_length, 'none'], 'min-height': css_length, 'min-width': css_length, opacity: css_number, outline: [true, 'outline-color', 'outline-style', 'outline-width'], 'outline-color': ['invert', css_color], 'outline-style': [ 'dashed', 'dotted', 'double', 'groove', 'inset', 'none', 'outset', 'ridge', 'solid' ], 'outline-width': css_width, overflow: css_overflow, 'overflow-x': css_overflow, 'overflow-y': css_overflow, padding: [4, css_length], 'padding-bottom': css_length, 'padding-left': css_length, 'padding-right': css_length, 'padding-top': css_length, 'page-break-after': css_break, 'page-break-before': css_break, position: ['absolute', 'fixed', 'relative', 'static'], quotes: [8, css_string], right: [css_length, 'auto'], 'table-layout': ['auto', 'fixed'], 'text-align': ['center', 'justify', 'left', 'right'], 'text-decoration': [ 'none', 'underline', 'overline', 'line-through', 'blink' ], 'text-indent': css_length, 'text-shadow': ['none', 4, [css_color, css_length]], 'text-transform': ['capitalize', 'uppercase', 'lowercase', 'none'], top: [css_length, 'auto'], 'unicode-bidi': ['normal', 'embed', 'bidi-override'], 'vertical-align': [ 'baseline', 'bottom', 'sub', 'super', 'top', 'text-top', 'middle', 'text-bottom', css_length ], visibility: ['visible', 'hidden', 'collapse'], 'white-space': [ 'normal', 'nowrap', 'pre', 'pre-line', 'pre-wrap', 'inherit' ], width: [css_length, 'auto'], 'word-spacing': ['normal', css_length], 'word-wrap': ['break-word', 'normal'], 'z-index': ['auto', css_number] }; function style_attribute() { var v; while (next_token.id === '*' || next_token.id === '#' || next_token.string === '_') { if (!option.css) { warn('unexpected_a'); } advance(); } if (next_token.id === '-') { if (!option.css) { warn('unexpected_a'); } advance('-'); if (!next_token.identifier) { warn('expected_nonstandard_style_attribute'); } advance(); return css_any; } else { if (!next_token.identifier) { warn('expected_style_attribute'); } else { if (Object.prototype.hasOwnProperty.call(css_attribute_data, next_token.string)) { v = css_attribute_data[next_token.string]; } else { v = css_any; if (!option.css) { warn('unrecognized_style_attribute_a'); } } } advance(); return v; } } function style_value(v) { /*jslint confusion: true */ var i = 0, n, once, match, round, start = 0, vi; switch (typeof v) { case 'function': return v(); case 'string': if (next_token.identifier && next_token.string === v) { advance(); return true; } return false; } for (;;) { if (i >= v.length) { return false; } vi = v[i]; i += 1; if (typeof vi === 'boolean') { break; } else if (typeof vi === 'number') { n = vi; vi = v[i]; i += 1; } else { n = 1; } match = false; while (n > 0) { if (style_value(vi)) { match = true; n -= 1; } else { break; } } if (match) { return true; } } start = i; once = []; for (;;) { round = false; for (i = start; i < v.length; i += 1) { if (!once[i]) { if (style_value(css_attribute_data[v[i]])) { match = true; round = true; once[i] = true; break; } } } if (!round) { return match; } } } function style_child() { if (next_token.id === '(number)') { advance(); if (next_token.string === 'n' && next_token.identifier) { no_space_only(); advance(); if (next_token.id === '+') { no_space_only(); advance('+'); no_space_only(); advance('(number)'); } } return; } else { if (next_token.identifier && (next_token.string === 'odd' || next_token.string === 'even')) { advance(); return; } } warn('unexpected_a'); } function substyle() { var v; for (;;) { if (next_token.id === '}' || next_token.id === '(end)' || (xquote && next_token.id === xquote)) { return; } v = style_attribute(); advance(':'); if (next_token.identifier && next_token.string === 'inherit') { advance(); } else { if (!style_value(v)) { warn('unexpected_a'); advance(); } } if (next_token.id === '!') { advance('!'); no_space_only(); if (next_token.identifier && next_token.string === 'important') { advance(); } else { warn('expected_a_b', next_token, 'important', artifact()); } } if (next_token.id === '}' || next_token.id === xquote) { warn('expected_a_b', next_token, ';', artifact()); } else { semicolon(); } } } function style_selector() { if (next_token.identifier) { if (!Object.prototype.hasOwnProperty.call(html_tag, option.cap ? next_token.string.toLowerCase() : next_token.string)) { warn('expected_tagname_a'); } advance(); } else { switch (next_token.id) { case '>': case '+': advance(); style_selector(); break; case ':': advance(':'); switch (next_token.string) { case 'active': case 'after': case 'before': case 'checked': case 'disabled': case 'empty': case 'enabled': case 'first-child': case 'first-letter': case 'first-line': case 'first-of-type': case 'focus': case 'hover': case 'last-child': case 'last-of-type': case 'link': case 'only-of-type': case 'root': case 'target': case 'visited': advance_identifier(next_token.string); break; case 'lang': advance_identifier('lang'); advance('('); if (!next_token.identifier) { warn('expected_lang_a'); } advance(')'); break; case 'nth-child': case 'nth-last-child': case 'nth-last-of-type': case 'nth-of-type': advance_identifier(next_token.string); advance('('); style_child(); advance(')'); break; case 'not': advance_identifier('not'); advance('('); if (next_token.id === ':' && peek(0).string === 'not') { warn('not'); } style_selector(); advance(')'); break; default: warn('expected_pseudo_a'); } break; case '#': advance('#'); if (!next_token.identifier) { warn('expected_id_a'); } advance(); break; case '*': advance('*'); break; case '.': advance('.'); if (!next_token.identifier) { warn('expected_class_a'); } advance(); break; case '[': advance('['); if (!next_token.identifier) { warn('expected_attribute_a'); } advance(); if (next_token.id === '=' || next_token.string === '~=' || next_token.string === '$=' || next_token.string === '|=' || next_token.id === '*=' || next_token.id === '^=') { advance(); if (next_token.id !== '(string)') { warn('expected_string_a'); } advance(); } advance(']'); break; default: stop('expected_selector_a'); } } } function style_pattern() { if (next_token.id === '{') { warn('expected_style_pattern'); } for (;;) { style_selector(); if (next_token.id === '</' || next_token.id === '{' || next_token.id === '}' || next_token.id === '(end)') { return ''; } if (next_token.id === ',') { comma(); } } } function style_list() { while (next_token.id !== '}' && next_token.id !== '</' && next_token.id !== '(end)') { style_pattern(); xmode = 'styleproperty'; if (next_token.id === ';') { semicolon(); } else { advance('{'); substyle(); xmode = 'style'; advance('}'); } } } function styles() { var i; while (next_token.id === '@') { i = peek(); advance('@'); switch (next_token.string) { case 'import': advance_identifier('import'); if (!css_url()) { warn('expected_a_b', next_token, 'url', artifact()); advance(); } semicolon(); break; case 'media': advance_identifier('media'); for (;;) { if (!next_token.identifier || css_media[next_token.string] !== true) { stop('expected_media_a'); } advance(); if (next_token.id !== ',') { break; } comma(); } advance('{'); style_list(); advance('}'); break; case 'font-face': advance_identifier('font-face'); advance('{'); font_face(); advance('}'); break; default: stop('expected_at_a'); } } style_list(); } // Parse HTML function do_begin(n) { if (n !== 'html' && !option.fragment) { if (n === 'div' && option.adsafe) { stop('adsafe_fragment'); } else { stop('expected_a_b', token, 'html', n); } } if (option.adsafe) { if (n === 'html') { stop('adsafe_html', token); } if (option.fragment) { if (n !== 'div') { stop('adsafe_div', token); } } else { stop('adsafe_fragment', token); } } option.browser = true; } function do_attribute(a, v) { var u, x; if (a === 'id') { u = typeof v === 'string' ? v.toUpperCase() : ''; if (ids[u] === true) { warn('duplicate_a', next_token, v); } if (!/^[A-Za-z][A-Za-z0-9._:\-]*$/.test(v)) { warn('bad_id_a', next_token, v); } else if (option.adsafe) { if (adsafe_id) { if (v.slice(0, adsafe_id.length) !== adsafe_id) { warn('adsafe_prefix_a', next_token, adsafe_id); } else if (!/^[A-Z]+_[A-Z]+$/.test(v)) { warn('adsafe_bad_id'); } } else { adsafe_id = v; if (!/^[A-Z]+_$/.test(v)) { warn('adsafe_bad_id'); } } } x = v.search(dx); if (x >= 0) { warn('unexpected_char_a_b', token, v.charAt(x), a); } ids[u] = true; } else if (a === 'class' || a === 'type' || a === 'name') { x = v.search(qx); if (x >= 0) { warn('unexpected_char_a_b', token, v.charAt(x), a); } ids[u] = true; } else if (a === 'href' || a === 'background' || a === 'content' || a === 'data' || a.indexOf('src') >= 0 || a.indexOf('url') >= 0) { if (option.safe && ux.test(v)) { stop('bad_url_a', next_token, v); } urls.push(v); } else if (a === 'for') { if (option.adsafe) { if (adsafe_id) { if (v.slice(0, adsafe_id.length) !== adsafe_id) { warn('adsafe_prefix_a', next_token, adsafe_id); } else if (!/^[A-Z]+_[A-Z]+$/.test(v)) { warn('adsafe_bad_id'); } } else { warn('adsafe_bad_id'); } } } else if (a === 'name') { if (option.adsafe && v.indexOf('_') >= 0) { warn('adsafe_name_a', next_token, v); } } } function do_tag(name, attribute) { var i, tag = html_tag[name], script, x; src = false; if (!tag) { stop( bundle.unrecognized_tag_a, next_token, name === name.toLowerCase() ? name : name + ' (capitalization error)' ); } if (stack.length > 0) { if (name === 'html') { stop('unexpected_a', token, name); } x = tag.parent; if (x) { if (x.indexOf(' ' + stack[stack.length - 1].name + ' ') < 0) { stop('tag_a_in_b', token, name, x); } } else if (!option.adsafe && !option.fragment) { i = stack.length; do { if (i <= 0) { stop('tag_a_in_b', token, name, 'body'); } i -= 1; } while (stack[i].name !== 'body'); } } switch (name) { case 'div': if (option.adsafe && stack.length === 1 && !adsafe_id) { warn('adsafe_missing_id'); } break; case 'script': xmode = 'script'; advance('>'); if (attribute.lang) { warn('lang', token); } if (option.adsafe && stack.length !== 1) { warn('adsafe_placement', token); } if (attribute.src) { if (option.adsafe && (!adsafe_may || !approved[attribute.src])) { warn('adsafe_source', token); } if (attribute.type) { warn('type', token); } } else { step_in(next_token.from); edge(); use_strict(); adsafe_top = true; script = statements(); // JSLint is also the static analyzer for ADsafe. See www.ADsafe.org. if (option.adsafe) { if (adsafe_went) { stop('adsafe_script', token); } if (script.length !== 1 || aint(script[0], 'id', '(') || aint(script[0].first, 'id', '.') || aint(script[0].first.first, 'string', 'ADSAFE') || aint(script[0].second[0], 'string', adsafe_id)) { stop('adsafe_id_go'); } switch (script[0].first.second.string) { case 'id': if (adsafe_may || adsafe_went || script[0].second.length !== 1) { stop('adsafe_id', next_token); } adsafe_may = true; break; case 'go': if (adsafe_went) { stop('adsafe_go'); } if (script[0].second.length !== 2 || aint(script[0].second[1], 'id', 'function') || !script[0].second[1].first || aint(script[0].second[1].first[0], 'string', 'dom') || script[0].second[1].first.length > 2 || (script[0].second[1].first.length === 2 && aint(script[0].second[1].first[1], 'string', 'lib'))) { stop('adsafe_go', next_token); } adsafe_went = true; break; default: stop('adsafe_id_go'); } } indent = null; } xmode = 'html'; advance('</'); advance_identifier('script'); xmode = 'outer'; break; case 'style': xmode = 'style'; advance('>'); styles(); xmode = 'html'; advance('</'); advance_identifier('style'); break; case 'input': switch (attribute.type) { case 'button': case 'checkbox': case 'radio': case 'reset': case 'submit': break; case 'file': case 'hidden': case 'image': case 'password': case 'text': if (option.adsafe && attribute.autocomplete !== 'off') { warn('adsafe_autocomplete'); } break; default: warn('bad_type'); } break; case 'applet': case 'body': case 'embed': case 'frame': case 'frameset': case 'head': case 'iframe': case 'noembed': case 'noframes': case 'object': case 'param': if (option.adsafe) { warn('adsafe_tag', next_token, name); } break; } } function closetag(name) { return '</' + name + '>'; } function html() { /*jslint confusion: true */ var attribute, attributes, is_empty, name, old_white = option.white, quote, tag_name, tag, wmode; xmode = 'html'; xquote = ''; stack = null; for (;;) { switch (next_token.string) { case '<': xmode = 'html'; advance('<'); attributes = {}; tag_name = next_token; name = tag_name.string; advance_identifier(name); if (option.cap) { name = name.toLowerCase(); } tag_name.name = name; if (!stack) { stack = []; do_begin(name); } tag = html_tag[name]; if (typeof tag !== 'object') { stop('unrecognized_tag_a', tag_name, name); } is_empty = tag.empty; tag_name.type = name; for (;;) { if (next_token.id === '/') { advance('/'); if (next_token.id !== '>') { warn('expected_a_b', next_token, '>', artifact()); } break; } if (next_token.id && next_token.id.charAt(0) === '>') { break; } if (!next_token.identifier) { if (next_token.id === '(end)' || next_token.id === '(error)') { warn('expected_a_b', next_token, '>', artifact()); } warn('bad_name_a'); } option.white = false; spaces(); attribute = next_token.string; option.white = old_white; advance(); if (!option.cap && attribute !== attribute.toLowerCase()) { warn('attribute_case_a', token); } attribute = attribute.toLowerCase(); xquote = ''; if (Object.prototype.hasOwnProperty.call(attributes, attribute)) { warn('duplicate_a', token, attribute); } if (attribute.slice(0, 2) === 'on') { if (!option.on) { warn('html_handlers'); } xmode = 'scriptstring'; advance('='); quote = next_token.id; if (quote !== '"' && quote !== '\'') { stop('expected_a_b', next_token, '"', artifact()); } xquote = quote; wmode = option.white; option.white = true; advance(quote); use_strict(); statements(); option.white = wmode; if (next_token.id !== quote) { stop('expected_a_b', next_token, quote, artifact()); } xmode = 'html'; xquote = ''; advance(quote); tag = false; } else if (attribute === 'style') { xmode = 'scriptstring'; advance('='); quote = next_token.id; if (quote !== '"' && quote !== '\'') { stop('expected_a_b', next_token, '"', artifact()); } xmode = 'styleproperty'; xquote = quote; advance(quote); substyle(); xmode = 'html'; xquote = ''; advance(quote); tag = false; } else { if (next_token.id === '=') { advance('='); tag = next_token.string; if (!next_token.identifier && next_token.id !== '"' && next_token.id !== '\'' && next_token.id !== '(string)' && next_token.id !== '(string)' && next_token.id !== '(color)') { warn('expected_attribute_value_a', token, attribute); } advance(); } else { tag = true; } } attributes[attribute] = tag; do_attribute(attribute, tag); } do_tag(name, attributes); if (!is_empty) { stack.push(tag_name); } xmode = 'outer'; advance('>'); break; case '</': xmode = 'html'; advance('</'); if (!next_token.identifier) { warn('bad_name_a'); } name = next_token.string; if (option.cap) { name = name.toLowerCase(); } advance(); if (!stack) { stop('unexpected_a', next_token, closetag(name)); } tag_name = stack.pop(); if (!tag_name) { stop('unexpected_a', next_token, closetag(name)); } if (tag_name.name !== name) { stop('expected_a_b', next_token, closetag(tag_name.name), closetag(name)); } if (next_token.id !== '>') { stop('expected_a_b', next_token, '>', artifact()); } xmode = 'outer'; advance('>'); break; case '<!': if (option.safe) { warn('adsafe_a'); } xmode = 'html'; for (;;) { advance(); if (next_token.id === '>' || next_token.id === '(end)') { break; } if (next_token.string.indexOf('--') >= 0) { stop('unexpected_a', next_token, '--'); } if (next_token.string.indexOf('<') >= 0) { stop('unexpected_a', next_token, '<'); } if (next_token.string.indexOf('>') >= 0) { stop('unexpected_a', next_token, '>'); } } xmode = 'outer'; advance('>'); break; case '(end)': return; default: if (next_token.id === '(end)') { stop('missing_a', next_token, '</' + stack[stack.length - 1].string + '>'); } else { advance(); } } if (stack && stack.length === 0 && (option.adsafe || !option.fragment || next_token.id === '(end)')) { break; } } if (next_token.id !== '(end)') { stop('unexpected_a'); } } // The actual JSLINT function itself. itself = function JSLint(the_source, the_option) { var i, predef, tree; JSLINT.errors = []; JSLINT.tree = ''; begin = prev_token = token = next_token = Object.create(syntax['(begin)']); predefined = {}; add_to_predefined(standard); property_type = Object.create(standard_property_type); if (the_option) { option = Object.create(the_option); predef = option.predef; if (predef) { if (Array.isArray(predef)) { for (i = 0; i < predef.length; i += 1) { predefined[predef[i]] = true; } } else if (typeof predef === 'object') { add_to_predefined(predef); } } do_safe(); } else { option = {}; } option.indent = +option.indent || 4; option.maxerr = +option.maxerr || 50; adsafe_id = ''; adsafe_may = adsafe_top = adsafe_went = false; approved = {}; if (option.approved) { for (i = 0; i < option.approved.length; i += 1) { approved[option.approved[i]] = option.approved[i]; } } else { approved.test = 'test'; } tab = ''; for (i = 0; i < option.indent; i += 1) { tab += ' '; } global_scope = scope = {}; global_funct = funct = { '(scope)': scope, '(breakage)': 0, '(loopage)': 0 }; functions = [funct]; comments_off = false; ids = {}; in_block = false; indent = null; json_mode = false; lookahead = []; member = {}; node_js = false; prereg = true; src = false; stack = null; strict_mode = false; urls = []; var_mode = null; warnings = 0; xmode = ''; lex.init(the_source); assume(); try { advance(); if (next_token.id === '(number)') { stop('unexpected_a'); } else if (next_token.string.charAt(0) === '<') { html(); if (option.adsafe && !adsafe_went) { warn('adsafe_go', this); } } else { switch (next_token.id) { case '{': case '[': json_mode = true; json_value(); break; case '@': case '*': case '#': case '.': case ':': xmode = 'style'; advance(); if (token.id !== '@' || !next_token.identifier || next_token.string !== 'charset' || token.line !== 1 || token.from !== 1) { stop('css'); } advance(); if (next_token.id !== '(string)' && next_token.string !== 'UTF-8') { stop('css'); } advance(); semicolon(); styles(); break; default: if (option.adsafe && option.fragment) { stop('expected_a_b', next_token, '<div>', artifact()); } // If the first token is a semicolon, ignore it. This is sometimes used when // files are intended to be appended to files that may be sloppy. A sloppy // file may be depending on semicolon insertion on its last line. step_in(1); if (next_token.id === ';' && !node_js) { semicolon(); } adsafe_top = true; tree = statements(); begin.first = tree; JSLINT.tree = begin; // infer_types(tree); if (option.adsafe && (tree.length !== 1 || aint(tree[0], 'id', '(') || aint(tree[0].first, 'id', '.') || aint(tree[0].first.first, 'string', 'ADSAFE') || aint(tree[0].first.second, 'string', 'lib') || tree[0].second.length !== 2 || tree[0].second[0].id !== '(string)' || aint(tree[0].second[1], 'id', 'function'))) { stop('adsafe_lib'); } if (tree.disrupt) { warn('weird_program', prev_token); } } } indent = null; advance('(end)'); } catch (e) { if (e) { // `~ JSLINT.errors.push({ reason : e.message, line : e.line || next_token.line, character : e.character || next_token.from }, null); } } return JSLINT.errors.length === 0; }; // Data summary. itself.data = function () { var data = {functions: []}, function_data, globals, i, j, kind, members = [], name, the_function, undef = [], unused = []; if (itself.errors.length) { data.errors = itself.errors; } if (json_mode) { data.json = true; } if (urls.length > 0) { data.urls = urls; } globals = Object.keys(global_scope).filter(function (value) { return value.charAt(0) !== '(' && typeof standard[value] !== 'boolean'; }); if (globals.length > 0) { data.globals = globals; } for (i = 1; i < functions.length; i += 1) { the_function = functions[i]; function_data = {}; for (j = 0; j < functionicity.length; j += 1) { function_data[functionicity[j]] = []; } for (name in the_function) { if (Object.prototype.hasOwnProperty.call(the_function, name)) { if (name.charAt(0) !== '(') { kind = the_function[name]; if (kind === 'unction' || kind === 'unparam') { kind = 'unused'; } if (Array.isArray(function_data[kind])) { function_data[kind].push(name); if (kind === 'unused') { unused.push({ name: name, line: the_function['(line)'], 'function': the_function['(name)'] }); } else if (kind === 'undef') { undef.push({ name: name, line: the_function['(line)'], 'function': the_function['(name)'] }); } } } } } for (j = 0; j < functionicity.length; j += 1) { if (function_data[functionicity[j]].length === 0) { delete function_data[functionicity[j]]; } } function_data.name = the_function['(name)']; function_data.params = the_function['(params)']; function_data.line = the_function['(line)']; data.functions.push(function_data); } if (unused.length > 0) { data.unused = unused; } if (undef.length > 0) { data['undefined'] = undef; } members = []; for (name in member) { if (typeof member[name] === 'number') { data.member = member; break; } } return data; }; itself.report = function (errors_only) { var data = itself.data(), err, evidence, i, italics, j, key, keys, length, mem = '', name, names, output = [], snippets, the_function, type, warning; function detail(h, value) { var comma_needed, singularity; if (Array.isArray(value)) { output.push('<div><i>' + h + '</i> '); value.sort().forEach(function (item) { if (item !== singularity) { singularity = item; output.push((comma_needed ? ', ' : '') + singularity); comma_needed = true; } }); output.push('</div>'); } else if (value) { output.push('<div><i>' + h + '</i> ' + value + '</div>'); } } if (data.errors || data.unused || data['undefined']) { err = true; output.push('<div id=errors><i>Error:</i>'); if (data.errors) { for (i = 0; i < data.errors.length; i += 1) { warning = data.errors[i]; if (warning) { evidence = warning.evidence || ''; output.push('<p>Problem' + (isFinite(warning.line) ? ' at line ' + String(warning.line) + ' character ' + String(warning.character) : '') + ': ' + warning.reason.entityify() + '</p><p class=evidence>' + (evidence && (evidence.length > 80 ? evidence.slice(0, 77) + '...' : evidence).entityify()) + '</p>'); } } } if (data['undefined']) { snippets = []; for (i = 0; i < data['undefined'].length; i += 1) { snippets[i] = '<code><u>' + data['undefined'][i].name + '</u></code> <i>' + String(data['undefined'][i].line) + ' </i> <small>' + data['undefined'][i]['function'] + '</small>'; } output.push('<p><i>Undefined variable:</i> ' + snippets.join(', ') + '</p>'); } if (data.unused) { snippets = []; for (i = 0; i < data.unused.length; i += 1) { snippets[i] = '<code><u>' + data.unused[i].name + '</u></code> <i>' + String(data.unused[i].line) + ' </i> <small>' + data.unused[i]['function'] + '</small>'; } output.push('<p><i>Unused variable:</i> ' + snippets.join(', ') + '</p>'); } if (data.json) { output.push('<p>JSON: bad.</p>'); } output.push('</div>'); } if (!errors_only) { output.push('<br><div id=functions>'); if (data.urls) { detail("URLs<br>", data.urls, '<br>'); } if (xmode === 'style') { output.push('<p>CSS.</p>'); } else if (data.json && !err) { output.push('<p>JSON: good.</p>'); } else if (data.globals) { output.push('<div><i>Global</i> ' + data.globals.sort().join(', ') + '</div>'); } else { output.push('<div><i>No new global variables introduced.</i></div>'); } for (i = 0; i < data.functions.length; i += 1) { the_function = data.functions[i]; names = []; if (the_function.params) { for (j = 0; j < the_function.params.length; j += 1) { names[j] = the_function.params[j].string; } } output.push('<br><div class=function><i>' + String(the_function.line) + '</i> ' + the_function.name.entityify() + '(' + names.join(', ') + ')</div>'); detail('<big><b>Undefined</b></big>', the_function['undefined']); detail('<big><b>Unused</b></big>', the_function.unused); detail('Closure', the_function.closure); detail('Variable', the_function['var']); detail('Exception', the_function.exception); detail('Outer', the_function.outer); detail('Global', the_function.global); detail('Label', the_function.label); } if (data.member) { keys = Object.keys(data.member); if (keys.length) { keys = keys.sort(); output.push('<br><pre id=properties>/*properties<br>'); mem = ' '; italics = 0; j = 0; if (option.confusion) { for (i = 0; i < keys.length; i += 1) { key = keys[i]; if (typeof standard_property_type[key] !== 'string') { name = ix.test(key) ? key : '\'' + key.entityify().replace(nx, sanitize) + '\''; if (data.member[key] === 1) { name = '<i>' + name + '</i>'; italics += 1; j = 1; } if (i < keys.length - 1) { name += ', '; } if (mem.length + name.length - (italics * 7) > 80) { output.push(mem + '<br>'); mem = ' '; italics = j; } mem += name; j = 0; } } } else { for (i = 0; i < keys.length; i += 1) { key = keys[i]; type = property_type[key]; if (typeof type !== 'string') { type = ''; } if (standard_property_type[key] !== type) { name = ix.test(key) ? key : '\'' + key.entityify().replace(nx, sanitize) + '\''; length += name.length + 2; if (data.member[key] === 1) { name = '<i>' + name + '</i>'; italics += 1; j = 1; } if (type) { name += ': ' + type; } if (i < keys.length - 1) { name += ', '; } if (mem.length + name.length - (italics * 7) > 80) { output.push(mem + '<br>'); mem = ' '; italics = j; } mem += name; j = 0; } } } output.push(mem + '<br>*/</pre>'); } output.push('</div>'); } } return output.join(''); }; itself.jslint = itself; itself.edition = '2012-01-13'; return itself; }()); define("thirdparty/jslint/jslint", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, JSLINT, PathUtils */ /** * Allows JSLint to run on the current document and report results in a UI panel. * */ define('language/JSLintUtils',['require','exports','module','thirdparty/path-utils/path-utils.min','thirdparty/jslint/jslint','command/Commands','command/CommandManager','document/DocumentManager','preferences/PreferencesManager','utils/PerfUtils','strings','editor/EditorManager'],function (require, exports, module) { // Load dependent non-module scripts require("thirdparty/path-utils/path-utils.min"); require("thirdparty/jslint/jslint"); // Load dependent modules var Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), DocumentManager = require("document/DocumentManager"), PreferencesManager = require("preferences/PreferencesManager"), PerfUtils = require("utils/PerfUtils"), Strings = require("strings"), EditorManager = require("editor/EditorManager"); /** * @private * @type {PreferenceStorage} */ var _prefs = null; /** * @private * @type {boolean} */ var _enabled = true; /** * @return {boolean} Enabled state of JSLint. */ function getEnabled() { return _enabled; } /** * Run JSLint on the current document. Reports results to the main UI. Displays * a gold star when no errors are found. */ function run() { var currentDoc = DocumentManager.getCurrentDocument(); var perfTimerDOM, perfTimerLint; var ext = currentDoc ? PathUtils.filenameExtension(currentDoc.file.fullPath) : ""; var $lintResults = $("#jslint-results"); var $lintStatus = $("#lint-status"); if (getEnabled() && /^(\.js|\.htm|\.html)$/i.test(ext)) { perfTimerLint = PerfUtils.markStart("JSLint linting:\t" + (!currentDoc || currentDoc.file.fullPath)); var text = currentDoc.getText(); // If a line contains only whitespace, remove the whitespace // This should be doable with a regexp: text.replace(/\r[\x20|\t]+\r/g, "\r\r");, // but that doesn't work. var i, arr = text.split("\n"); for (i = 0; i < arr.length; i++) { if (!arr[i].match(/\S/)) { arr[i] = ""; } } text = arr.join("\n"); var result = JSLINT(text, null); PerfUtils.addMeasurement(perfTimerLint); perfTimerDOM = PerfUtils.markStart("JSLint DOM:\t" + (!currentDoc || currentDoc.file.fullPath)); if (!result) { var $errorTable = $("<table class='zebra-striped condensed-table' />") .append("<tbody>"); var $selectedRow; JSLINT.errors.forEach(function (item, i) { if (item) { var makeCell = function (content) { return $("<td/>").text(content); }; // Add row to error table var $row = $("<tr/>") .append(makeCell(item.line)) .append(makeCell(item.reason)) .append(makeCell(item.evidence || "")) .appendTo($errorTable); $row.click(function () { if ($selectedRow) { $selectedRow.removeClass("selected"); } $row.addClass("selected"); $selectedRow = $row; var editor = EditorManager.getCurrentFullEditor(); editor.setCursorPos(item.line - 1, item.character - 1); EditorManager.focusEditor(); }); } }); $("#jslint-results .table-container") .empty() .append($errorTable); $lintResults.show(); $lintStatus.hide(); } else { $lintResults.hide(); $lintStatus.show(); } PerfUtils.addMeasurement(perfTimerDOM); } else { // JSLint is disabled or does not apply to the current file, hide // both the results and the gold star $lintResults.hide(); $lintStatus.hide(); } EditorManager.resizeEditor(); } /** * @private * Update DocumentManager listeners. */ function _updateListeners() { if (_enabled) { // register our event listeners $(DocumentManager) .on("currentDocumentChange.jslint", function () { run(); }) .on("documentSaved.jslint", function (event, document) { if (document === DocumentManager.getCurrentDocument()) { run(); } }); } else { $(DocumentManager).off(".jslint"); } } function _setEnabled(enabled) { _enabled = enabled; CommandManager.get(Commands.TOGGLE_JSLINT).setChecked(_enabled); _updateListeners(); _prefs.setValue("enabled", _enabled); // run immediately run(); } /** * Enable or disable JSLint. * @param {boolean} enabled Enabled state. */ function setEnabled(enabled) { if (_enabled !== enabled) { _setEnabled(enabled); } } /** Command to toggle enablement */ function _handleToggleJSLint() { setEnabled(!getEnabled()); } // Register command handlers CommandManager.register(Strings.CMD_JSLINT, Commands.TOGGLE_JSLINT, _handleToggleJSLint); // Init PreferenceStorage _prefs = PreferencesManager.getPreferenceStorage(module.id, { enabled: true }); _setEnabled(_prefs.getValue("enabled")); // Define public API exports.run = run; exports.getEnabled = getEnabled; exports.setEnabled = setEnabled; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ /* * Displays an auto suggest pop-up list of files to allow the user to quickly navigate to a file and lines * within a file. * Uses FileIndexManger to supply the file list. * * TODO (issue 333) - currently jquery smart auto complete is used for the pop-up list. While it mostly works * it has several issues, so it should be replace with an alternative. Issues: * - only accepts an array of strings. A list of objects is preferred to avoid some workarounds to display * both the path and filename. * - the pop-up position logic has flaws that require CSS workarounds * - the pop-up properties cannot be modified once the object is constructed */ define('search/QuickOpen',['require','exports','module','project/FileIndexManager','document/DocumentManager','editor/EditorManager','command/CommandManager','strings','utils/StringUtils','command/Commands','project/ProjectManager'],function (require, exports, module) { var FileIndexManager = require("project/FileIndexManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), CommandManager = require("command/CommandManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), Commands = require("command/Commands"), ProjectManager = require("project/ProjectManager"); /** @type Array.<QuickOpenPlugin> */ var plugins = []; /** @type {QuickOpenPlugin} */ var currentPlugin = null; /** @type Array.<FileInfo>*/ var fileList; /** * Remembers the current document that was displayed when showDialog() was called * The current document is restored if the user presses escape * @type {string} full path */ var origDocPath; /** * Remembers the selection in the document origDocPath that was present when showDialog() was called. * Focusing on an item can cause the current and and/or selection to change, so this variable restores it. * The cursor position is restored if the user presses escape. * @type ?{start:{line:number, ch:number}, end:{line:number, ch:number}} */ var origSelection; var dialogOpen = false; /** * Defines API for new QuickOpen plug-ins */ function QuickOpenPlugin(name, fileTypes, done, search, match, itemFocus, itemSelect, resultsFormatter) { this.name = name; this.fileTypes = fileTypes; this.done = done; this.search = search; this.match = match; this.itemFocus = itemFocus; this.itemSelect = itemSelect; this.resultsFormatter = resultsFormatter; } /** * Creates and registers a new QuickOpenPlugin * * @param { name: string, * fileTypes:Array.<string>} plugin, * done: function(), * search: function(string):Array.<string>, * match: function(string):boolean, * itemFocus: functon(HTMLLIElement), * itemSelect: functon(HTMLLIElement), * resultsFormatter: ?Functon(string, string):string } * * @returns {QuickOpenPlugin} plugin * * Parameter Documentation: * * name - plug-in name * filetypes - file types array. Example: ["js", "css", "txt"]. An empty array * indicates all file types. * done - called when quick open is complete. Plug-in should clear its internal state. * search - takes a query string and returns an array of strings that match the query. * match - takes a query string and returns true if this plug-in wants to provide * results for this query. * itemFocus - performs an action when a result has focus. * The focused HTMLLIElement is passed as an argument. * itemSelect - performs an action when a result is chosen. * The selected HTMLLIElement is passed as an argument. * resultFormatter - takes a query string and an item string and returns * a <LI> item to insert into the displayed search results. If null, default is provided. */ function addQuickOpenPlugin(pluginDef) { plugins.push(new QuickOpenPlugin( pluginDef.name, pluginDef.fileTypes, pluginDef.done, pluginDef.search, pluginDef.match, pluginDef.itemFocus, pluginDef.itemSelect, pluginDef.resultsFormatter )); } /** * QuickNavigateDialog class * @constructor */ function QuickNavigateDialog() { this.$searchField = undefined; // defined when showDialog() is called } /** * Creates a dialog div floating on top of the current code mirror editor */ QuickNavigateDialog.prototype._createDialogDiv = function (template) { this.dialog = $("<div />") .attr("class", "CodeMirror-dialog") .html("<div align='right'>" + template + "</div>") .prependTo($("#editor-holder")); }; function _filenameFromPath(path, includeExtension) { var end; if (includeExtension) { end = path.length; } else { end = path.lastIndexOf("."); } return path.slice(path.lastIndexOf("/") + 1, end); } /** * Attempts to extract a line number from the query where the line number * is followed by a colon. Callers should explicitly test result with isNaN() * * @param {string} query string to extract line number from * @returns {number} line number. Returns NaN to indicate no line numbeer was found */ function extractLineNumber(query) { // only match : at beginning of query for now // TODO: match any location of : when QuickOpen._handleItemFocus() is modified to // dynamic open files if (query.indexOf(":") !== 0) { return NaN; } var result = NaN; var regInfo = query.match(/(!?:)(\d+)/); // colon followed by a digit if (regInfo) { result = regInfo[2] - 1; } return result; } /** * Navigates to the appropriate file and file location given the selected item * and closes the dialog. * * Note, if selectedItem is null quick search should inspect $searchField for text * that may have not matched anything in in the list, but may have information * for carrying out an action. */ QuickNavigateDialog.prototype._handleItemSelect = function (selectedItem) { // This is a work-around to select first item when a selection event occurs // (usually from pressing the enter key) and no item is selected in the list. // This is a work-around since Smart auto complete doesn't select the first item if (!selectedItem) { selectedItem = $(".smart_autocomplete_container > li:first-child").get(0); } // Delegate to current plugin if (currentPlugin) { currentPlugin.itemSelect(selectedItem); } else { // extract line number var cursor, query = this.$searchField.val(), gotoLine = extractLineNumber(query); if (!isNaN(gotoLine)) { cursor = {line: gotoLine, ch: 0}; } // Extract file path var fullPath; if (selectedItem) { fullPath = decodeURIComponent($(selectedItem).attr("data-fullpath")); } // Nagivate to file and line number if (fullPath) { CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, {fullPath: fullPath}) .done(function () { if (!isNaN(gotoLine)) { EditorManager.getCurrentFullEditor().setCursorPos(cursor); } }); } else if (!isNaN(gotoLine)) { EditorManager.getCurrentFullEditor().setCursorPos(cursor); } } this._close(); EditorManager.focusEditor(); }; /** * Opens the file specified by selected item if there is no current plug-in, otherwise defers handling * to the currentPlugin */ QuickNavigateDialog.prototype._handleItemFocus = function (selectedItem) { if (currentPlugin) { currentPlugin.itemFocus(selectedItem); } // TODO: Disable opening files on focus for now since this causes focus related bugs between // the editor and the search field. // Also, see related code in _handleItemFocus /* else { var fullPath = $(selectedItem).attr("data-fullpath"); if (fullPath) { fullPath = decodeURIComponent(fullPath); CommandManager.execute(Commands.FILE_OPEN, {fullPath: fullPath, focusEditor: false}); } } */ }; /** * KeyUp is for cases that handle AFTER a character has been committed to $searchField * */ QuickNavigateDialog.prototype._handleKeyUp = function (e) { var query = this.$searchField.val(); // extract line number var gotoLine = extractLineNumber(query); if (!isNaN(gotoLine)) { var from = {line: gotoLine, ch: 0}; var to = {line: gotoLine, ch: 99999}; EditorManager.getCurrentFullEditor().setSelection(from, to); } // Remove current plugin if the query stops matching if (currentPlugin && !currentPlugin.match(query)) { currentPlugin = null; } if ($(".smart_autocomplete_highlight").length === 0) { this._handleItemFocus($(".smart_autocomplete_container > li:first-child")); } }; /** * Close the dialog when the ENTER (13) or ESC (27) key is pressed * * Note, when keydown is handled $searchField does not yet have the character added * for the current event e. */ QuickNavigateDialog.prototype._handleKeyDown = function (e) { // TODO: pass event through KeyMap.translateKeyboardEvent() to get friendly names // instead of using these constants here. Note, translateKeyboardEvent() doesn't yet // make friendly names for the escape and enter key. var ESCKey = 27, EnterKey = 13; // clear the query on ESC key and restore document and cursor position if (e.keyCode === EnterKey || e.keyCode === ESCKey) { e.stopPropagation(); e.preventDefault(); if (e.keyCode === ESCKey) { // restore previously viewed doc if user navigated away from it if (origDocPath) { CommandManager.execute(Commands.FILE_OPEN, {fullPath: origDocPath}) .done(function () { if (origSelection) { EditorManager.getCurrentFullEditor().setSelection(origSelection.start, origSelection.end); } }); } this._close(); } if (e.keyCode === EnterKey) { this._handleItemSelect($(".smart_autocomplete_highlight").get(0)); } } }; /** * Closes the search dialog and notifies all quick open plugins that * searching is done. */ QuickNavigateDialog.prototype._close = function () { if (!dialogOpen) { return; } dialogOpen = false; var i; for (i = 0; i < plugins.length; i++) { var plugin = plugins[i]; plugin.done(); } // Ty TODO: disabled for now while file switching is disabled in _handleItemFocus //JSLintUtils.setEnabled(true); EditorManager.focusEditor(); this.dialog.remove(); $(".smart_autocomplete_container").remove(); $(window.document).off("mousedown", this.handleDocumentClick); }; function filterFileList(query) { var filteredList = $.map(fileList, function (fileInfo) { // match query against filename only (not the full path) var path = fileInfo.fullPath; var filename = _filenameFromPath(path, true); if (filename.toLowerCase().indexOf(query.toLowerCase()) !== -1) { return path; } else { return null; } }).sort(function (a, b) { a = a.toLowerCase(); b = b.toLowerCase(); //first, sort by filename without extension var filenameA = _filenameFromPath(a, false); var filenameB = _filenameFromPath(b, false); if (filenameA < filenameB) { return -1; } else if (filenameA > filenameB) { return 1; } else { // filename is the same, compare including extension filenameA = _filenameFromPath(a, true); filenameB = _filenameFromPath(b, true); if (filenameA < filenameB) { return -1; } else if (filenameA > filenameB) { return 1; } else { return 0; } } }); return filteredList; } function _handleFilter(query) { var curDoc = DocumentManager.getCurrentDocument(); if (curDoc) { var filename = _filenameFromPath(curDoc.file.fullPath, true); var extension = filename.slice(filename.lastIndexOf(".") + 1, filename.length); var i; for (i = 0; i < plugins.length; i++) { var plugin = plugins[i]; var extensionMatch = plugin.fileTypes.indexOf(extension) !== -1 || plugin.fileTypes.length === 0; if (extensionMatch && plugin.match && plugin.match(query)) { currentPlugin = plugin; return plugin.search(query); } } } currentPlugin = null; return filterFileList(query); } function defaultResultsFormatter(item, query) { query = query.slice(query.indexOf("@") + 1, query.length); // Escape both query and item so the replace works properly below query = StringUtils.htmlEscape(query); item = StringUtils.htmlEscape(item); var displayName; if (query.length > 0) { // make the users query bold within the item's text displayName = item.replace( new RegExp(StringUtils.regexEscape(query), "gi"), "<strong>$&</strong>" ); } else { displayName = item; } return "<li>" + displayName + "</li>"; } function _handleResultsFormatter(item) { var query = $("input#quickOpenSearch").val(); if (currentPlugin) { // Plugins use their own formatter or the default formatter var formatter = currentPlugin.resultsFormatter || defaultResultsFormatter; return formatter(item, query); } else { // Use the filename formatter query = StringUtils.htmlEscape(query); var filename = StringUtils.htmlEscape(_filenameFromPath(item, true)); var rPath = StringUtils.htmlEscape(ProjectManager.makeProjectRelativeIfPossible(item)); var displayName; if (query.length > 0) { // make the users query bold within the item's text displayName = filename.replace( new RegExp(StringUtils.regexEscape(query), "gi"), "<strong>$&</strong>" ); } else { displayName = filename; } return "<li data-fullpath='" + encodeURIComponent(item) + "'>" + displayName + "<br /><span class='quick-open-path'>" + rPath + "</span></li>"; } } function setSearchFieldValue(prefix, initialString) { prefix = prefix || ""; initialString = initialString || ""; initialString = prefix + initialString; var $field = $("input#quickOpenSearch"); if ($field) { $field.val(initialString); $field.get(0).setSelectionRange(prefix.length, initialString.length); } } /** * Close the dialog when the user clicks outside of it. Note, auto smart complete has a "lostFocus" event that is * supposed to capture this event, but it also gets triggered on keyUp which doesn't work for quick find. */ QuickNavigateDialog.prototype.handleDocumentClick = function (e) { if ($(this.dialog).find(e.target).length === 0 && $(".smart_autocomplete_container").find(e.target).length === 0) { this._close(); } }; /** * Shows the search dialog and initializes the auto suggestion list with filenames from the current project */ QuickNavigateDialog.prototype.showDialog = function (prefix, initialString) { var that = this; if (dialogOpen) { return; } dialogOpen = true; this.handleDocumentClick = this.handleDocumentClick.bind(this); $(window.document).on("mousedown", this.handleDocumentClick); // Ty TODO: disabled for now while file switching is disabled in _handleItemFocus // To improve performance during list selection disable JSLint until a document is chosen or dialog is closed //JSLintUtils.setEnabled(false); var curDoc = DocumentManager.getCurrentDocument(); origDocPath = curDoc ? curDoc.file.fullPath : null; if (curDoc) { origSelection = EditorManager.getCurrentFullEditor().getSelection(); } else { origSelection = null; } // Get the file list and initialize the smart auto completes FileIndexManager.getFileInfoList("all") .done(function (files) { fileList = files; var dialogHTML = Strings.CMD_QUICK_OPEN + ": <input type='text' autocomplete='off' id='quickOpenSearch' style='width: 30em'>"; that._createDialogDiv(dialogHTML); that.$searchField = $("input#quickOpenSearch"); that.$searchField.smartAutoComplete({ source: files, maxResults: 20, minCharLimit: 0, autocompleteFocused: true, forceSelect: false, typeAhead: false, // won't work right now because smart auto complete // using internal raw results instead of filtered results for matching filter: _handleFilter, resultFormatter: _handleResultsFormatter }); that.$searchField.bind({ itemSelect: function (e, selectedItem) { that._handleItemSelect(selectedItem); }, itemFocus: function (e, selectedItem) { that._handleItemFocus(selectedItem); }, keydown: function (e) { that._handleKeyDown(e); }, keyup: function (e, query) { that._handleKeyUp(e); } // Note: lostFocus event DOESN'T work because auto smart complete catches the key up from shift-command-o and immediately // triggers lostFocus }); setSearchFieldValue(prefix, initialString); }); }; function getCurrentEditorSelectedText() { var currentEditor = EditorManager.getFocusedEditor(); return (currentEditor && currentEditor.getSelectedText()) || ""; } function doSearch(prefix, initialString) { if (dialogOpen) { setSearchFieldValue(prefix, initialString); } else { var dialog = new QuickNavigateDialog(); dialog.showDialog(prefix, initialString); } } function doFileSearch() { doSearch("", getCurrentEditorSelectedText()); } function doGotoLine() { // TODO: Brackets doesn't support disabled menu items right now, when it does goto line and // goto definition should be disabled when there is not a current document if (DocumentManager.getCurrentDocument()) { doSearch(":", ""); } } // TODO: should provide a way for QuickOpenJSSymbol to create this function as a plug-in function doDefinitionSearch() { if (DocumentManager.getCurrentDocument()) { doSearch("@", getCurrentEditorSelectedText()); } } // TODO: allow QuickOpenJS to register it's own commands and key bindings CommandManager.register(Strings.CMD_QUICK_OPEN, Commands.NAVIGATE_QUICK_OPEN, doFileSearch); CommandManager.register(Strings.CMD_GOTO_DEFINITION, Commands.NAVIGATE_GOTO_DEFINITION, doDefinitionSearch); CommandManager.register(Strings.CMD_GOTO_LINE, Commands.NAVIGATE_GOTO_LINE, doGotoLine); exports.addQuickOpenPlugin = addQuickOpenPlugin; }); /** * @license RequireJS text 1.0.8 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/requirejs for details */ /*jslint regexp: true, plusplus: true, sloppy: true */ /*global require: false, XMLHttpRequest: false, ActiveXObject: false, define: false, window: false, process: false, Packages: false, java: false, location: false */ (function () { var progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, bodyRegExp = /<body[^>]*>\s*([\s\S]+)\s*<\/body>/im, hasLocation = typeof location !== 'undefined' && location.href, defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), defaultHostName = hasLocation && location.hostname, defaultPort = hasLocation && (location.port || undefined), buildMap = []; define('text',[],function () { var text, fs; text = { version: '1.0.8', strip: function (content) { //Strips <?xml ...?> declarations so that external SVG and XML //documents can be added to a document without worry. Also, if the string //is an HTML document, only the part inside the body tag is returned. if (content) { content = content.replace(xmlRegExp, ""); var matches = content.match(bodyRegExp); if (matches) { content = matches[1]; } } else { content = ""; } return content; }, jsEscape: function (content) { return content.replace(/(['\\])/g, '\\$1') .replace(/[\f]/g, "\\f") .replace(/[\b]/g, "\\b") .replace(/[\n]/g, "\\n") .replace(/[\t]/g, "\\t") .replace(/[\r]/g, "\\r"); }, createXhr: function () { //Would love to dump the ActiveX crap in here. Need IE 6 to die first. var xhr, i, progId; if (typeof XMLHttpRequest !== "undefined") { return new XMLHttpRequest(); } else if (typeof ActiveXObject !== "undefined") { for (i = 0; i < 3; i++) { progId = progIds[i]; try { xhr = new ActiveXObject(progId); } catch (e) {} if (xhr) { progIds = [progId]; // so faster next time break; } } } return xhr; }, /** * Parses a resource name into its component parts. Resource names * look like: module/name.ext!strip, where the !strip part is * optional. * @param {String} name the resource name * @returns {Object} with properties "moduleName", "ext" and "strip" * where strip is a boolean. */ parseName: function (name) { var strip = false, index = name.indexOf("."), modName = name.substring(0, index), ext = name.substring(index + 1, name.length); index = ext.indexOf("!"); if (index !== -1) { //Pull off the strip arg. strip = ext.substring(index + 1, ext.length); strip = strip === "strip"; ext = ext.substring(0, index); } return { moduleName: modName, ext: ext, strip: strip }; }, xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, /** * Is an URL on another domain. Only works for browser use, returns * false in non-browser environments. Only used to know if an * optimized .js version of a text resource should be loaded * instead. * @param {String} url * @returns Boolean */ useXhr: function (url, protocol, hostname, port) { var match = text.xdRegExp.exec(url), uProtocol, uHostName, uPort; if (!match) { return true; } uProtocol = match[2]; uHostName = match[3]; uHostName = uHostName.split(':'); uPort = uHostName[1]; uHostName = uHostName[0]; return (!uProtocol || uProtocol === protocol) && (!uHostName || uHostName === hostname) && ((!uPort && !uHostName) || uPort === port); }, finishLoad: function (name, strip, content, onLoad, config) { content = strip ? text.strip(content) : content; if (config.isBuild) { buildMap[name] = content; } onLoad(content); }, load: function (name, req, onLoad, config) { //Name has format: some.module.filext!strip //The strip part is optional. //if strip is present, then that means only get the string contents //inside a body tag in an HTML string. For XML/SVG content it means //removing the <?xml ...?> declarations so the content can be inserted //into the current doc without problems. // Do not bother with the work if a build and text will // not be inlined. if (config.isBuild && !config.inlineText) { onLoad(); return; } var parsed = text.parseName(name), nonStripName = parsed.moduleName + '.' + parsed.ext, url = req.toUrl(nonStripName), useXhr = (config && config.text && config.text.useXhr) || text.useXhr; //Load the text. Use XHR if possible and in a browser. if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { text.get(url, function (content) { text.finishLoad(name, parsed.strip, content, onLoad, config); }); } else { //Need to fetch the resource across domains. Assume //the resource has been optimized into a JS module. Fetch //by the module name + extension, but do not include the //!strip part to avoid file system issues. req([nonStripName], function (content) { text.finishLoad(parsed.moduleName + '.' + parsed.ext, parsed.strip, content, onLoad, config); }); } }, write: function (pluginName, moduleName, write, config) { if (buildMap.hasOwnProperty(moduleName)) { var content = text.jsEscape(buildMap[moduleName]); write.asModule(pluginName + "!" + moduleName, "define(function () { return '" + content + "';});\n"); } }, writeFile: function (pluginName, moduleName, req, write, config) { var parsed = text.parseName(moduleName), nonStripName = parsed.moduleName + '.' + parsed.ext, //Use a '.js' file name so that it indicates it is a //script that can be loaded across domains. fileName = req.toUrl(parsed.moduleName + '.' + parsed.ext) + '.js'; //Leverage own load() method to load plugin value, but only //write out values that do not have the strip argument, //to avoid any potential issues with ! in file names. text.load(nonStripName, req, function (value) { //Use own write() method to construct full module value. //But need to create shell that translates writeFile's //write() to the right interface. var textWrite = function (contents) { return write(fileName, contents); }; textWrite.asModule = function (moduleName, contents) { return write.asModule(moduleName, fileName, contents); }; text.write(pluginName, nonStripName, textWrite, config); }, config); } }; if (text.createXhr()) { text.get = function (url, callback) { var xhr = text.createXhr(); xhr.open('GET', url, true); xhr.onreadystatechange = function (evt) { //Do not explicitly handle errors, those should be //visible via console output in the browser. if (xhr.readyState === 4) { callback(xhr.responseText); } }; xhr.send(null); }; } else if (typeof process !== "undefined" && process.versions && !!process.versions.node) { //Using special require.nodeRequire, something added by r.js. fs = require.nodeRequire('fs'); text.get = function (url, callback) { var file = fs.readFileSync(url, 'utf8'); //Remove BOM (Byte Mark Order) from utf8 files if it is there. if (file.indexOf('\uFEFF') === 0) { file = file.substring(1); } callback(file); }; } else if (typeof Packages !== 'undefined') { //Why Java, why is this so awkward? text.get = function (url, callback) { var encoding = "utf-8", file = new java.io.File(url), lineSeparator = java.lang.System.getProperty("line.separator"), input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), stringBuffer, line, content = ''; try { stringBuffer = new java.lang.StringBuffer(); line = input.readLine(); // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 // http://www.unicode.org/faq/utf_bom.html // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 if (line && line.length() && line.charAt(0) === 0xfeff) { // Eat the BOM, since we've already found the encoding on this file, // and we plan to concatenating this buffer with others; the BOM should // only appear at the top of a file. line = line.substring(1); } stringBuffer.append(line); while ((line = input.readLine()) !== null) { stringBuffer.append(lineSeparator); stringBuffer.append(line); } //Make sure we return a JavaScript string and not a Java string. content = String(stringBuffer.toString()); //String } finally { input.close(); } callback(content); }; } return text; }); }()); define('text!htmlContent/main-view.html',[],function () { return '<!-- \n Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.\n \n Permission is hereby granted, free of charge, to any person obtaining a\n copy of this software and associated documentation files (the "Software"), \n to deal in the Software without restriction, including without limitation \n the rights to use, copy, modify, merge, publish, distribute, sublicense, \n and/or sell copies of the Software, and to permit persons to whom the \n Software is furnished to do so, subject to the following conditions:\n \n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n \n THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, \n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER \n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING \n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER \n DEALINGS IN THE SOFTWARE.\n-->\n\n\n<!--\n This is the HTML for the body tag of index.html. It is loaded dynamically by the \n htmlContentLoad module and localized using a combination of mustache.js and i18n.js.\n\n LOCALIZATION NOTE:\n All display text for this file must use templating so the text can be localized.\n \n English text goes in src/nls/root/strings.js. All other translations go in the strings.js file for\n the specific local in the nls folder. If a translation is missing for a specific key English\n is used as a fallback\n\n Strings should be referenced using the double brackets syntax.\n Example: {{keyname}}. Note, all strings are HTML escaped unless the form \n {{&keyname}} is used.\n\n-->\n\n<!-- Main UI -->\n <div class="main-view">\n <div id="sidebar-resizer"></div>\n <div id="sidebar" class="sidebar quiet-scrollbars">\n <!-- Left-hand \'Project panel\' -->\n <div id="projects" class="panel">\n <div id="project-header"></div>\n <div id="file-section">\n <div id="open-files-container">\n <!-- This will contain a dynamically generated <ul> at runtime -->\n <ul>\n </ul>\n </div>\n \n <div id="project-files-header" class="project-file-header-area">\n <span id="project-title" class="title"></span>\n </div>\n <div id="project-files-container">\n <!-- This will contain a dynamically generated <ul> hierarchy at runtime -->\n </div>\n </div>\n </div>\n </div>\n \n <!-- Right-hand content: toolbar, editor, bottom panels -->\n <div class="content">\n <!-- Toolbar containing menus, filename, and icons -->\n <div id="main-toolbar" class="toolbar">\n <!-- Menu bar -->\n <ul class="nav" data-dropdown="dropdown">\n </ul>\n \n <!-- Toolbar -->\n <div class="buttons">\n <span id="update-notification" title="{{UPDATE_NOTIFICATION_TOOLTIP}}"></span>\n\n <span class="experimental-label">{{EXPERIMENTAL_BUILD}}</span>\n \n <a href="#" id="toolbar-go-live"></a> <!-- tooltip for this is set in JS -->\n \n <span id="gold-star" title="No JSLint errors - good job!">\n ★\n </span>\n </div>\n \n <!-- Filename label -->\n <div class="title-wrapper">\n <span class="title"></span> <span class=\'dirty-dot\' style="visibility:hidden;">•</span>\n </div>\n </div>\n \n <div id="editor-holder">\n <div id="not-editor">\n <div id="not-editor-content">[ ]</div>\n </div>\n </div>\n \n <div id="jslint-results" class="bottom-panel">\n <div class="toolbar simple-toolbar-layout">\n <div class="title">{{JSLINT_ERRORS}}</div>\n </div>\n <div class="table-container"></div>\n </div>\n <div id="search-results" class="bottom-panel">\n <div class="toolbar simple-toolbar-layout">\n <div class="title">{{SEARCH_RESULTS}}</div>\n <div class="title" id="search-result-summary"></div>\n <a href="#" class="close">×</a>\n </div>\n <div class="table-container"></div>\n </div>\n </div>\n </div>\n\n <!-- Modal Windows -->\n <div class="error-dialog template modal hide">\n <div class="modal-header">\n <a href="#" class="close">×</a>\n <h1 class="dialog-title">Error</h1>\n </div>\n <div class="modal-body">\n <p class="dialog-message">Message goes here</p>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn primary" data-button-id="ok">{{OK}}</a>\n </div>\n </div>\n <div class="save-close-dialog template modal hide">\n <div class="modal-header">\n <a href="#" class="close">×</a>\n <h1 class="dialog-title">{{SAVE_CHANGES}}</h1>\n </div>\n <div class="modal-body">\n <p class="dialog-message">Message goes here</p>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn left" data-button-id="dontsave">{{DONT_SAVE}}</a>\n <a href="#" class="dialog-button btn primary" data-button-id="ok">{{SAVE}}</a>\n <a href="#" class="dialog-button btn" data-button-id="cancel">{{CANCEL}}</a>\n </div>\n </div>\n <div class="ext-changed-dialog template modal hide">\n <div class="modal-header">\n <h1 class="dialog-title">Title goes here</h1>\n </div>\n <div class="modal-body">\n <p class="dialog-message">Message goes here</p>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn left" data-button-id="dontsave">{{RELOAD_FROM_DISK}}</a>\n <a href="#" class="dialog-button btn primary" data-button-id="cancel">{{KEEP_CHANGES_IN_EDITOR}}</a>\n </div>\n </div>\n <div class="ext-deleted-dialog template modal hide">\n <div class="modal-header">\n <h1 class="dialog-title">Title goes here</h1>\n </div>\n <div class="modal-body">\n <p class="dialog-message">Message goes here</p>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn left" data-button-id="dontsave">{{CLOSE_DONT_SAVE}}</a>\n <a href="#" class="dialog-button btn primary" data-button-id="cancel">{{KEEP_CHANGES_IN_EDITOR}}</a>\n </div>\n </div>\n <div class="live-development-error-dialog template modal hide">\n <div class="modal-header">\n <h1 class="dialog-title">Title goes here</h1>\n </div>\n <div class="modal-body">\n <p class="dialog-message">Message goes here</p>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn left" data-button-id="cancel">{{CANCEL}}</a>\n <a href="#" class="dialog-button btn primary" data-button-id="ok">{{RELAUNCH_CHROME}}</a>\n </div>\n </div>\n <div class="about-dialog template modal hide">\n <div class="modal-header">\n <h1 class="dialog-title">{{ABOUT}}</h1>\n </div>\n <div class="modal-body">\n <img class="about-icon" src="styles/images/brackets_icon.svg">\n <div class="about-text">\n <h2>{{BRACKETS}}</h2>\n <p class="dialog-message">{{ABOUT_TEXT_LINE1}} <span id="about-build-number"><!-- populated programmatically --></span></p>\n <p class="dialog-message">{{ABOUT_TEXT_LINE2}}</p>\n <p class="dialog-message">{{ABOUT_TEXT_LINE3}}<span class="non-clickble-link">http://www.adobe.com/go/thirdparty/</span>{{ABOUT_TEXT_LINE4}}</p>\n <p class="dialog-message">{{ABOUT_TEXT_LINE5}}<span class="non-clickble-link">https://github.com/adobe/brackets/</span></p>\n </div>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn primary" data-button-id="ok">{{CLOSE}}</a>\n </div>\n </div>\n <div class="update-dialog template modal hide">\n <div class="modal-header">\n <h1 class="dialog-title">{{UPDATE_AVAILABLE_TITLE}}</h1>\n </div>\n <div class="modal-body">\n <img class="update-icon" src="styles/images/update_large_icon.svg">\n <div class="update-text">\n <p class="dialog-message">{{UPDATE_MESSAGE}}</p>\n <div class="update-info">\n </div>\n </div>\n </div>\n <div class="modal-footer">\n <a href="#" class="dialog-button btn left" data-button-id="cancel">{{CANCEL}}</a>\n <a href="#" class="dialog-button btn primary" data-button-id="download">{{GET_IT_NOW}}</a>\n </div>\n </div>\n <div id="context-menu-bar">\n <ul data-dropdown="dropdown"></ul>\n </div>\n <div id="codehint-menu-bar">\n <ul data-dropdown="dropdown"></ul>\n </div>';}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, brackets, window */ /** * ExtensionLoader searches the filesystem for extensions, then creates a new context for each one and loads it */ define('utils/ExtensionLoader',['require','exports','module','file/NativeFileSystem','file/FileUtils','utils/Async'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, FileUtils = require("file/FileUtils"), Async = require("utils/Async"), contexts = {}; /** * Returns the require.js require context used to load an extension * * @param {!string} name, used to identify the extension * @return {!Object} A require.js require object used to load the extension, or undefined if * there is no require object ith that name */ function getRequireContextForExtension(name) { return contexts[name]; } /** * Loads the extension that lives at baseUrl into its own Require.js context * * @param {!string} name, used to identify the extension * @param {!string} baseUrl, URL path relative to index.html, where the main JS file can be found * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when the extension is loaded. */ function loadExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionRequire = brackets.libRequire.config({ context: name, baseUrl: config.baseUrl, /* FIXME (issue #1087): can we pass this from the global require context instead of hardcoding twice? */ paths: { "text" : "../../../thirdparty/text", "i18n" : "../../../thirdparty/i18n" }, locale: window.localStorage.getItem("locale") || brackets.app.language }); contexts[name] = extensionRequire; console.log("[Extension] starting to load " + config.baseUrl); extensionRequire([entryPoint], function () { console.log("[Extension] finished loading " + config.baseUrl); result.resolve(); }); return result.promise(); } /** * Runs unit tests for the extension that lives at baseUrl into its own Require.js context * * @param {!string} name, used to identify the extension * @param {!string} baseUrl, URL path relative to index.html, where the main JS file can be found * @param {!string} entryPoint, name of the main js file to load * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function testExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionPath = FileUtils.getNativeBracketsDirectoryPath(); // Assumes the caller's window.location context is /test/SpecRunner.html extensionPath = extensionPath.replace("brackets/test", "brackets/src"); // convert from "test" to "src" extensionPath += "/" + config.baseUrl + "/" + entryPoint + ".js"; var fileExists = false, statComplete = false; brackets.fs.stat(extensionPath, function (err, stat) { statComplete = true; if (err === brackets.fs.NO_ERROR && stat.isFile()) { // unit test file exists var extensionRequire = brackets.libRequire.config({ context: name, baseUrl: "../src/" + config.baseUrl, paths: config.paths }); console.log("[Extension] loading unit test " + config.baseUrl); extensionRequire([entryPoint], function () { console.log("[Extension] loaded unit tests " + config.baseUrl); result.resolve(); }); } else { result.reject(); } }); return result.promise(); } /** * @private * Loads a file entryPoint from each extension folder within the baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @param {!string} baseUrl, URL path relative to index.html that maps to the same place as directory * @param {!string} entryPoint Module name to load (without .js suffix) * @param {function} processExtension * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function _loadAll(directory, config, entryPoint, processExtension) { var result = new $.Deferred(); NativeFileSystem.requestNativeFileSystem(directory, function (rootEntry) { rootEntry.createReader().readEntries( function (entries) { var i, extensions = []; for (i = 0; i < entries.length; i++) { if (entries[i].isDirectory) { // FUTURE (JRB): read package.json instead of just using the entrypoint "main". // Also, load sub-extensions defined in package.json. extensions.push(entries[i].name); } } if (extensions.length === 0) { result.resolve(); return; } Async.doInParallel(extensions, function (item) { var extConfig = { baseUrl: config.baseUrl + "/" + item, paths: config.paths }; return processExtension(item, extConfig, entryPoint); }).always(function () { // Always resolve the promise even when the extension entry point is missing result.resolve(); }); }, function (error) { console.log("[Extension] Error -- could not read native directory: " + directory); } ); }, function (error) { console.log("[Extension] Error -- could not open native directory: " + directory); }); return result.promise(); } /** * Loads the extension that lives at baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @param {!string} baseUrl, URL path relative to index.html that maps to the same place as directory * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function loadAllExtensionsInNativeDirectory(directory, baseUrl) { return _loadAll(directory, {baseUrl: baseUrl}, "main", loadExtension); } /** * Runs unit test for the extension that lives at baseUrl into its own Require.js context * * @param {!string} directory, an absolute native path that contains a directory of extensions. * each subdirectory is interpreted as an independent extension * @param {!string} baseUrl, URL path relative to index.html that maps to the same place as directory * @return {!$.Promise} A promise object that is resolved when all extensions complete loading. */ function testAllExtensionsInNativeDirectory(directory, baseUrl) { var bracketsPath = FileUtils.getNativeBracketsDirectoryPath(), config = { baseUrl: baseUrl }; config.paths = { "perf": bracketsPath + "/perf", "spec": bracketsPath + "/spec" }; return _loadAll(directory, config, "unittests", testExtension); } exports.getRequireContextForExtension = getRequireContextForExtension; exports.loadExtension = loadExtension; exports.testExtension = testExtension; exports.loadAllExtensionsInNativeDirectory = loadAllExtensionsInNativeDirectory; exports.testAllExtensionsInNativeDirectory = testAllExtensionsInNativeDirectory; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, document, window, brackets */ define('project/SidebarView',['require','exports','module','project/ProjectManager','project/WorkingSetView','command/CommandManager','command/Commands','strings','preferences/PreferencesManager','editor/EditorManager'],function (require, exports, module) { var ProjectManager = require("project/ProjectManager"), WorkingSetView = require("project/WorkingSetView"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), PreferencesManager = require("preferences/PreferencesManager"), EditorManager = require("editor/EditorManager"); var isSidebarClosed = false; var PREFERENCES_CLIENT_ID = "com.adobe.brackets.SidebarView", defaultPrefs = { sidebarWidth: 200, sidebarClosed: false }; // These vars are initialized by the htmlContentLoadComplete handler // below since they refer to DOM elements var $sidebar, $sidebarMenuText, $sidebarResizer, $openFilesContainer, $projectTitle, $projectFilesContainer; /** * @private * Update project title when the project root changes */ function _updateProjectTitle() { $projectTitle.html(ProjectManager.getProjectRoot().name); $projectTitle.attr("title", ProjectManager.getProjectRoot().fullPath); } /** * @private * Sets sidebar width and resizes editor. Does not change internal sidebar open/closed state. * @param {number} width Optional width in pixels. If null or undefined, the default width is used. * @param {!boolean} updateMenu Updates "View" menu label to indicate current sidebar state. * @param {!boolean} displayTriangle Display selection marker triangle in the active view. */ function _setWidth(width, updateMenu, displayTriangle) { // if we specify a width with the handler call, use that. Otherwise use // the greater of the current width or 200 (200 is the minimum width we'd snap back to) var prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs), sidebarWidth = Math.max(prefs.getValue("sidebarWidth"), 10); width = width || Math.max($sidebar.width(), sidebarWidth); if (typeof displayTriangle === "boolean") { var display = (displayTriangle) ? "block" : "none"; $sidebar.find(".sidebar-selection-triangle").css("display", display); } if (isSidebarClosed) { $sidebarResizer.css("left", 0); } else { $sidebar.width(width); $sidebarResizer.css("left", width - 1); // the following three lines help resize things when the sidebar shows // but ultimately these should go into ProjectManager.js with a "notify" // event that we can just call from anywhere instead of hard-coding it. // waiting on a ProjectManager refactor to add that. $sidebar.find(".sidebar-selection").width(width); if (width > 10) { prefs.setValue("sidebarWidth", width); } } if (updateMenu) { var text = (isSidebarClosed) ? Strings.CMD_SHOW_SIDEBAR : Strings.CMD_HIDE_SIDEBAR; CommandManager.get(Commands.VIEW_HIDE_SIDEBAR).setName(text); } EditorManager.resizeEditor(); } /** * Toggle sidebar visibility. */ function toggleSidebar(width) { if (isSidebarClosed) { $sidebar.show(); } else { $sidebar.hide(); } isSidebarClosed = !isSidebarClosed; var prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs); prefs.setValue("sidebarClosed", isSidebarClosed); _setWidth(width, true, !isSidebarClosed); } /** * @private * Install sidebar resize handling. */ function _initSidebarResizer() { var $mainView = $(".main-view"), $body = $(document.body), prefs = PreferencesManager.getPreferenceStorage(PREFERENCES_CLIENT_ID, defaultPrefs), sidebarWidth = prefs.getValue("sidebarWidth"), startingSidebarPosition = sidebarWidth, animationRequest = null, isMouseDown = false; $sidebarResizer.css("left", sidebarWidth - 1); if (prefs.getValue("sidebarClosed")) { toggleSidebar(sidebarWidth); } else { _setWidth(sidebarWidth, true, true); } $sidebarResizer.on("dblclick", function () { if ($sidebar.width() < 10) { //mousedown is fired first. Sidebar is already toggeled open to at least 10px. _setWidth(null, true, true); $projectFilesContainer.triggerHandler("scroll"); $openFilesContainer.triggerHandler("scroll"); } else { toggleSidebar(sidebarWidth); } }); $sidebarResizer.on("mousedown.sidebar", function (e) { var startX = e.clientX, newWidth = Math.max(e.clientX, 0), doResize = true; isMouseDown = true; // take away the shadows (for performance reasons during sidebarmovement) $sidebar.find(".scroller-shadow").css("display", "none"); $body.toggleClass("resizing"); // check to see if we're currently in hidden mode if (isSidebarClosed) { toggleSidebar(1); } animationRequest = window.webkitRequestAnimationFrame(function doRedraw() { // only run this if the mouse is down so we don't constantly loop even // after we're done resizing. if (!isMouseDown) { return; } // if we've gone below 10 pixels on a mouse move, and the // sidebar is shrinking, hide the sidebar automatically an // unbind the mouse event. if ((startX > 10) && (newWidth < 10)) { toggleSidebar(startingSidebarPosition); $mainView.off("mousemove.sidebar"); // turn off the mouseup event so that it doesn't fire twice and retoggle the // resizing class $mainView.off("mouseup.sidebar"); $body.toggleClass("resizing"); doResize = false; startX = 0; // force isMouseDown so that we don't keep calling requestAnimationFrame // this keeps the sidebar from stuttering isMouseDown = false; } if (doResize) { // for right now, displayTriangle is always going to be false for _setWidth // because we want to hide it when we move, and _setWidth only gets called // on mousemove now. _setWidth(newWidth, false, false); } animationRequest = window.webkitRequestAnimationFrame(doRedraw); }); $mainView.on("mousemove.sidebar", function (e) { newWidth = Math.max(e.clientX, 0); e.preventDefault(); }); $mainView.one("mouseup.sidebar", function (e) { isMouseDown = false; // replace shadows and triangle $sidebar.find(".sidebar-selection-triangle").css("display", "block"); $sidebar.find(".scroller-shadow").css("display", "block"); $projectFilesContainer.triggerHandler("scroll"); $openFilesContainer.triggerHandler("scroll"); $mainView.off("mousemove.sidebar"); $body.toggleClass("resizing"); startingSidebarPosition = $sidebar.width(); }); e.preventDefault(); }); } // Initialize items dependent on HTML DOM $(brackets).on("htmlContentLoadComplete", function () { $sidebar = $("#sidebar"); $sidebarMenuText = $("#menu-view-hide-sidebar span"); $sidebarResizer = $("#sidebar-resizer"); $openFilesContainer = $("#open-files-container"); $projectTitle = $("#project-title"); $projectFilesContainer = $("#project-files-container"); // init WorkingSetView.create($openFilesContainer); _initSidebarResizer(); }); $(ProjectManager).on("projectOpen", _updateProjectTitle); CommandManager.register(Strings.CMD_HIDE_SIDEBAR, Commands.VIEW_HIDE_SIDEBAR, toggleSidebar); // Define public API exports.toggleSidebar = toggleSidebar; }); define('text!buildNumber.json',[],function () { return '{\n "buildNumber": 100\n}\n';}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, PathUtils, window */ /** * Utilities functions for displaying update notifications * */ define('utils/UpdateNotification',['require','exports','module','widgets/Dialogs','utils/NativeApp','preferences/PreferencesManager','strings','utils/StringUtils','text!buildNumber.json'],function (require, exports, module) { var Dialogs = require("widgets/Dialogs"), NativeApp = require("utils/NativeApp"), PreferencesManager = require("preferences/PreferencesManager"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), BuildNumberJSON = require("text!buildNumber.json"); // Current build number. var _buildNumber = JSON.parse(BuildNumberJSON).buildNumber; // PreferenceStorage var _prefs = PreferencesManager.getPreferenceStorage(module.id, {lastNotifiedBuildNumber: 0}); // This is the last version we notified the user about. If checkForUpdate() // is called with "false", only show the update notification dialog if there // is an update newer than this one. This value is saved in preferences. var _lastNotifiedBuildNumber = _prefs.getValue("lastNotifiedBuildNumber"); // Last time the versionInfoURL was fetched var _lastInfoURLFetchTime = _prefs.getValue("lastInfoURLFetchTime"); // URL to load version info from. By default this is loaded no more than once a day. If // you force an update check it is always loaded. // URL to fetch the version information. var _versionInfoURL = "http://dev.brackets.io/updates/stable/"; // {locale}.json will be appended // Information on all posted builds of Brackets. This is an Array, where each element is // an Object with the following fields: // // {Number} buildNumber Number of the build // {String} versionString String representation of the build number (ie "Sprint 14") // {String} dateString Date of the build // {String} releaseNotesURL URL of the release notes for this build // {String} downloadURL URL to download this build // {Array} newFeatures Array of new features in this build. Each entry has two fields: // {String} name Name of the feature // {String} description Description of the feature // // This array must be reverse sorted by buildNumber (newest build info first) /** * @private * Flag that indicates if we've added a click handler to the update notification icon. */ var _addedClickHandler = false; /** * Get a data structure that has information for all builds of Brackets. * * If force is true, the information is always fetched from _versionInfoURL. * If force is false, we try to use cached information. If more than * 24 hours have passed since the last fetch, or if cached data can't be found, * the data is fetched again. * * If new data is fetched and dontCache is false, the data is saved in preferences * for quick fetching later. */ function _getUpdateInformation(force, dontCache) { var result = new $.Deferred(); var fetchData = false; var data; // If force is true, always fetch if (force) { fetchData = true; } // If we don't have data saved in prefs, fetch data = _prefs.getValue("updateInfo"); if (!data) { fetchData = true; } // If more than 24 hours have passed since our last fetch, fetch again if ((new Date()).getTime() > _lastInfoURLFetchTime + (1000 * 60 * 60 * 24)) { fetchData = true; } if (fetchData) { $.ajax(_versionInfoURL, { dataType: "text", complete: function (jqXHR, status) { if (status === "success") { try { data = JSON.parse(jqXHR.responseText); if (!dontCache) { _lastInfoURLFetchTime = (new Date()).getTime(); _prefs.setValue("lastInfoURLFetchTime", _lastInfoURLFetchTime); _prefs.setValue("updateInfo", data); } result.resolve(data); } catch (e) { console.log("Error parsing version information"); console.log(e); result.reject(); } } }, error: function (jqXHR, status, error) { // When loading data for unit tests, the error handler is // called but the responseText is valid. Try to use it here, // but *don't* save the results in prefs. if (!jqXHR.responseText) { // Text is NULL or empty string, reject(). result.reject(); return; } try { data = JSON.parse(jqXHR.responseText); result.resolve(data); } catch (e) { result.reject(); } } }); } else { result.resolve(data); } return result.promise(); } /** * Return a new array of version information that is newer than "buildNumber". * Returns null if there is no new version information. */ function _stripOldVersionInfo(versionInfo, buildNumber) { // Do a simple linear search. Since we are going in reverse-chronological order, we // should get through the search quickly. var lastIndex = 0; var len = versionInfo.length; while (lastIndex < len) { if (versionInfo[lastIndex].buildNumber <= buildNumber) { break; } lastIndex++; } if (lastIndex > 0) { return versionInfo.slice(0, lastIndex); } // No new version info return null; } /** * Show a dialog that shows the update */ function _showUpdateNotificationDialog(updates) { Dialogs.showModalDialog(Dialogs.DIALOG_ID_UPDATE) .done(function (id) { if (id === Dialogs.DIALOG_BTN_DOWNLOAD) { // The first entry in the updates array has the latest download link NativeApp.openURLInDefaultBrowser(updates[0].downloadURL); } }); // Populate the update data var $dlg = $(".update-dialog.instance"); var $updateList = $dlg.find(".update-info"); // TODO: Use a template instead of hand-rolling HTML code updates.forEach(function (item, index) { var $features = $("<ul>"); item.newFeatures.forEach(function (feature, index) { $features.append( "<li><b>" + StringUtils.htmlEscape(feature.name) + "</b> - " + StringUtils.htmlEscape(feature.description) + "</li>" ); }); var $item = $("<div>") .append("<h3>" + StringUtils.htmlEscape(item.versionString) + " - " + StringUtils.htmlEscape(item.dateString) + " (<a href='#' data-url='" + item.releaseNotesURL + "'>" + Strings.RELEASE_NOTES + "</a>)</h3>") .append($features) .appendTo($updateList); }); $dlg.on("click", "a", function (e) { var url = $(e.target).attr("data-url"); if (url) { // Make sure the URL has a domain that we know about if (/(brackets\.io|github\.com|adobe\.com)$/i.test(PathUtils.parseUrl(url).hostname)) { NativeApp.openURLInDefaultBrowser(url); } } }); } /** * Check for updates. If "force" is true, update notification dialogs are always displayed * (if an update is available). If "force" is false, the update notification is only * displayed for newly available updates. * * If an update is available, show the "update available" notification icon in the title bar. * * @param {boolean} force If true, always show the notification dialog. * @param {Object} _testValues This should only be used for testing purposes. See comments for details. * @return {$.Promise} jQuery Promise object that is resolved or rejected after the update check is complete. */ function checkForUpdate(force, _testValues) { // The second param, if non-null, is an Object containing value overrides. Values // in the object temporarily override the local values. This should *only* be used for testing. // If any overrides are set, permanent changes are not made (including showing // the update notification icon and saving prefs). var oldValues; var usingOverrides = false; // true if any of the values are overridden. var result = new $.Deferred(); if (_testValues) { oldValues = {}; if (_testValues.hasOwnProperty("_buildNumber")) { oldValues._buildNumber = _buildNumber; _buildNumber = _testValues._buildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("_lastNotifiedBuildNumber")) { oldValues._lastNotifiedBuildNumber = _lastNotifiedBuildNumber; _lastNotifiedBuildNumber = _testValues._lastNotifiedBuildNumber; usingOverrides = true; } if (_testValues.hasOwnProperty("_versionInfoURL")) { oldValues._versionInfoURL = _versionInfoURL; _versionInfoURL = _testValues._versionInfoURL; usingOverrides = true; } } _getUpdateInformation(force || usingOverrides, usingOverrides) .done(function (versionInfo) { // Get all available updates var allUpdates = _stripOldVersionInfo(versionInfo, _buildNumber); if (allUpdates) { // Always show the "update available" icon if any updates are available var $updateNotification = $("#update-notification"); $updateNotification.css("display", "inline-block"); if (!_addedClickHandler) { _addedClickHandler = true; $updateNotification.on("click", function () { checkForUpdate(true); }); } // Only show the update dialog if force = true, or if the user hasn't been // alerted of this update if (force || allUpdates[0].buildNumber > _lastNotifiedBuildNumber) { _showUpdateNotificationDialog(allUpdates); // Update prefs with the last notified build number _lastNotifiedBuildNumber = allUpdates[0].buildNumber; // Don't save prefs is we have overridden values if (!usingOverrides) { _prefs.setValue("lastNotifiedBuildNumber", _lastNotifiedBuildNumber); } } } else if (force) { // No updates are available. If force == true, let the user know. Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.NO_UPDATE_TITLE, Strings.NO_UPDATE_MESSAGE ); } if (oldValues) { if (oldValues.hasOwnProperty("_buildNumber")) { _buildNumber = oldValues._buildNumber; } if (oldValues.hasOwnProperty("_lastNotifiedBuildNumber")) { _lastNotifiedBuildNumber = oldValues._lastNotifiedBuildNumber; } if (oldValues.hasOwnProperty("_versionInfoURL")) { _versionInfoURL = oldValues._versionInfoURL; } } result.resolve(); }) .fail(function () { result.reject(); }); return result.promise(); } // Append locale to version info URL _versionInfoURL += (window.localStorage.getItem("locale") || brackets.app.language) + ".json"; // Define public API exports.checkForUpdate = checkForUpdate; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, window */ define('utils/UrlParams',['require','exports','module'],function (require, exports, module) { /** * Convert between URL querystring and name/value pairs. Decodes and encodes URL parameters. */ function UrlParams() { this._store = {}; } /** * Parse the window location by default. Optionally specify a URL to parse. * @param {string} url */ UrlParams.prototype.parse = function (url) { if (url) { url = url.substring(indexOf("?") + 1); } else { url = window.document.location.search.substring(1); } var urlParams = url.split("&"), p, self = this; urlParams.forEach(function (param) { p = param.split("="); self._store[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); }); }; /** * Store a name/value string pair * @param {!string} name * @param {!string} value */ UrlParams.prototype.put = function (name, value) { this._store[name] = value; }; /** * Retreive a value by name * @param {!string} name */ UrlParams.prototype.get = function (name) { return this._store[name]; }; /** * Encode name/value pairs as URI components. */ UrlParams.prototype.toString = function () { var strs = [], self = this; Object.keys(self._store).forEach(function (key) { strs.push(encodeURIComponent(key) + "=" + encodeURIComponent(self._store[key])); }); return strs.join("&"); }; // Define public API exports.UrlParams = UrlParams; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Text-editing commands that apply to whichever Editor is currently focused */ define('editor/EditorCommandHandlers',['require','exports','module','command/Commands','strings','command/CommandManager','editor/EditorManager'],function (require, exports, module) { // Load dependent modules var Commands = require("command/Commands"), Strings = require("strings"), CommandManager = require("command/CommandManager"), EditorManager = require("editor/EditorManager"); /** * List of constants */ var DIRECTION_UP = -1; var DIRECTION_DOWN = +1; /** * Add or remove line-comment tokens to all the lines in the selected range, preserving selection * and cursor position. Applies to currently focused Editor. * * If all non-whitespace lines are already commented out, then we uncomment; otherwise we comment * out. Commenting out adds "//" to at column 0 of every line. Uncommenting removes the first "//" * on each line (if any - empty lines might not have one). */ function lineCommentSlashSlash(editor) { var doc = editor.document; var sel = editor.getSelection(); var startLine = sel.start.line; var endLine = sel.end.line; // Is a range of text selected? (vs just an insertion pt) var hasSelection = (startLine !== endLine) || (sel.start.ch !== sel.end.ch); // In full-line selection, cursor pos is start of next line - but don't want to modify that line if (sel.end.ch === 0 && hasSelection) { endLine--; } // Decide if we're commenting vs. un-commenting // Are there any non-blank lines that aren't commented out? (We ignore blank lines because // some editors like Sublime don't comment them out) var containsUncommented = false; var i; var line; for (i = startLine; i <= endLine; i++) { line = doc.getLine(i); // A line is commented out if it starts with 0-N whitespace chars, then "//" if (!line.match(/^\s*\/\//) && line.match(/\S/)) { containsUncommented = true; break; } } // Make the edit doc.batchOperation(function () { if (containsUncommented) { // Comment out - prepend "//" to each line for (i = startLine; i <= endLine; i++) { doc.replaceRange("//", {line: i, ch: 0}); } // Make sure selection includes "//" that was added at start of range if (sel.start.ch === 0 && hasSelection) { // use *current* selection end, which has been updated for our text insertions editor.setSelection({line: startLine, ch: 0}, editor.getSelection().end); } } else { // Uncomment - remove first "//" on each line (if any) for (i = startLine; i <= endLine; i++) { line = doc.getLine(i); var commentI = line.indexOf("//"); if (commentI !== -1) { doc.replaceRange("", {line: i, ch: commentI}, {line: i, ch: commentI + 2}); } } } }); } /** * Invokes a language-specific line-comment/uncomment handler * @param {?Editor} editor If unspecified, applies to the currently focused editor */ function lineComment(editor) { editor = editor || EditorManager.getFocusedEditor(); if (!editor) { return; } var mode = editor.getModeForSelection(); // Currently we only support languages with "//" commenting if (mode === "javascript" || mode === "less") { lineCommentSlashSlash(editor); } } /** * Duplicates the selected text, or current line if no selection. The cursor/selection is left * on the second copy. */ function duplicateText(editor) { editor = editor || EditorManager.getFocusedEditor(); if (!editor) { return; } var sel = editor.getSelection(), hasSelection = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch), delimiter = ""; if (!hasSelection) { sel.start.ch = 0; sel.end = {line: sel.start.line + 1, ch: 0}; if (sel.end.line === editor.lineCount()) { delimiter = "\n"; } } // Make the edit var doc = editor.document; var selectedText = doc.getRange(sel.start, sel.end) + delimiter; doc.replaceRange(selectedText, sel.start); } /** * Moves the selected text, or current line if no selection. The cursor/selection * moves with the line/lines. * @param {Editor} editor - target editor * @param {Number} direction - direction of the move (-1,+1) => (Up,Down) */ function moveLine(editor, direction) { editor = editor || EditorManager.getFocusedEditor(); if (!editor) { return; } var doc = editor.document, sel = editor.getSelection(), originalSel = editor.getSelection(), hasSelection = (sel.start.line !== sel.end.line) || (sel.start.ch !== sel.end.ch); sel.start.ch = 0; // The end of the selection becomes the start of the next line, if it isn't already if (!hasSelection || sel.end.ch !== 0) { sel.end = {line: sel.end.line + 1, ch: 0}; } // Make the move switch (direction) { case DIRECTION_UP: if (sel.start.line !== 0) { doc.batchOperation(function () { var prevText = doc.getRange({ line: sel.start.line - 1, ch: 0 }, sel.start); if (sel.end.line === editor.lineCount()) { prevText = "\n" + prevText.substring(0, prevText.length - 1); } doc.replaceRange("", { line: sel.start.line - 1, ch: 0 }, sel.start); doc.replaceRange(prevText, { line: sel.end.line - 1, ch: 0 }); // Make sure CodeMirror hasn't expanded the selection to include // the line we inserted below. originalSel.start.line--; originalSel.end.line--; editor.setSelection(originalSel.start, originalSel.end); }); } break; case DIRECTION_DOWN: if (sel.end.line < editor.lineCount()) { doc.batchOperation(function () { var nextText = doc.getRange(sel.end, { line: sel.end.line + 1, ch: 0 }); var deletionStart = sel.end; if (sel.end.line === editor.lineCount() - 1) { nextText += "\n"; deletionStart = { line: sel.end.line - 1, ch: doc.getLine(sel.end.line - 1).length }; } doc.replaceRange("", deletionStart, { line: sel.end.line + 1, ch: 0 }); doc.replaceRange(nextText, { line: sel.start.line, ch: 0 }); }); } break; } } /** * Moves the selected text, or current line if no selection, one line up. The cursor/selection * moves with the line/lines. */ function moveLineUp(editor) { moveLine(editor, DIRECTION_UP); } /** * Moves the selected text, or current line if no selection, one line down. The cursor/selection * moves with the line/lines. */ function moveLineDown(editor) { moveLine(editor, DIRECTION_DOWN); } /** * Indent a line of text if no selection. Otherwise, indent all lines in selection. */ function indentText() { var editor = EditorManager.getFocusedEditor(); if (!editor) { return; } editor._codeMirror.execCommand("indentMore"); } /** * Unindent a line of text if no selection. Otherwise, unindent all lines in selection. */ function unidentText() { var editor = EditorManager.getFocusedEditor(); if (!editor) { return; } editor._codeMirror.execCommand("indentLess"); } // Register commands CommandManager.register(Strings.CMD_INDENT, Commands.EDIT_INDENT, indentText); CommandManager.register(Strings.CMD_UNINDENT, Commands.EDIT_UNINDENT, unidentText); CommandManager.register(Strings.CMD_COMMENT, Commands.EDIT_LINE_COMMENT, lineComment); CommandManager.register(Strings.CMD_DUPLICATE, Commands.EDIT_DUPLICATE, duplicateText); CommandManager.register(Strings.CMD_LINE_UP, Commands.EDIT_LINE_UP, moveLineUp); CommandManager.register(Strings.CMD_LINE_DOWN, Commands.EDIT_LINE_DOWN, moveLineDown); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window */ define('debug/DebugCommandHandlers',['require','exports','module','command/Commands','command/CommandManager','editor/Editor','strings','utils/PerfUtils','utils/NativeApp','file/NativeFileSystem','file/FileUtils','utils/UpdateNotification'],function (require, exports, module) { var Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), Editor = require("editor/Editor").Editor, Strings = require("strings"), PerfUtils = require("utils/PerfUtils"), NativeApp = require("utils/NativeApp"), NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, FileUtils = require("file/FileUtils"), UpdateNotification = require("utils/UpdateNotification"); function handleShowDeveloperTools(commandData) { brackets.app.showDeveloperTools(); } function _handleUseTabChars() { var useTabs = !Editor.getUseTabChar(); Editor.setUseTabChar(useTabs); CommandManager.get(Commands.TOGGLE_USE_TAB_CHARS).setChecked(useTabs); } // Implements the 'Run Tests' menu to bring up the Jasmine unit test window var _testWindow = null; function _handleRunUnitTests() { if (_testWindow) { try { _testWindow.location.reload(true); } catch (e) { _testWindow = null; // the window was probably closed } } if (!_testWindow) { _testWindow = window.open("../test/SpecRunner.html", "brackets-test", "width=" + $(window).width() + ",height=" + $(window).height()); _testWindow.location.reload(true); // if it was opened before, we need to reload because it will be cached } } function _handleShowPerfData() { var $perfHeader = $("<div class='modal-header' />") .append("<a href='#' class='close'>×</a>") .append("<h1 class='dialog-title'>Performance Data</h1>") .append("<div align=right>Raw data (copy paste out): <textarea rows=1 style='width:30px; height:8px; overflow: hidden; resize: none' id='brackets-perf-raw-data'>" + PerfUtils.getDelimitedPerfData() + "</textarea></div>"); var $perfBody = $("<div class='modal-body' style='padding: 0; max-height: 500px; overflow: auto;' />"); var $data = $("<table class='zebra-striped condensed-table'>") .append("<thead><th>Operation</th><th>Time (ms)</th></thead>") .append("<tbody />") .appendTo($perfBody); var makeCell = function (content) { return $("<td/>").text(content); }; var getValue = function (entry) { // entry is either an Array or a number if (Array.isArray(entry)) { // For Array of values, return: minimum/average/maximum/last var i, e, avg, sum = 0, min = Number.MAX_VALUE, max = 0; for (i = 0; i < entry.length; i++) { e = entry[i]; min = Math.min(min, e); sum += e; max = Math.max(max, e); } avg = Math.round(sum / entry.length); return String(min) + "/" + String(avg) + "/" + String(max) + "/" + String(e); } else { return entry; } }; var testName; var perfData = PerfUtils.getData(); for (testName in perfData) { if (perfData.hasOwnProperty(testName)) { // Add row to error table $("<tr/>") .append(makeCell(testName)) .append(makeCell(getValue(perfData[testName]))) .appendTo($data); } } $("<div class='modal hide' />") .append($perfHeader) .append($perfBody) .appendTo(window.document.body) .modal({ backdrop: "static", show: true }); // Select the raw perf data field on click since select all doesn't // work outside of the editor $("#brackets-perf-raw-data").click(function () { $(this).focus().select(); }); } function _handleNewBracketsWindow() { window.open(window.location.href); } function _handleSwitchLanguage() { var stringsPath = FileUtils.getNativeBracketsDirectoryPath() + "/nls"; NativeFileSystem.requestNativeFileSystem(stringsPath, function (dirEntry) { dirEntry.createReader().readEntries(function (entries) { var $activeLanguage, $submit, locale; function setLanguage(event) { if ($activeLanguage) { $activeLanguage.css("font-weight", "normal"); } $activeLanguage = $(event.currentTarget); locale = $activeLanguage.data("locale"); $activeLanguage.css("font-weight", "bold"); $submit.attr("disabled", false); } var $modal = $("<div class='modal hide' />"); var $header = $("<div class='modal-header' />") .append("<a href='#' class='close'>×</a>") .append("<h1 class='dialog-title'>" + Strings.LANGUAGE_TITLE + "</h1>") .appendTo($modal); var $body = $("<div class='modal-body' style='max-height: 500px; overflow: auto;' />") .appendTo($modal); var $p = $("<p class='dialog-message'>") .text(Strings.LANGUAGE_MESSAGE) .appendTo($body); var $ul = $("<ul>") .on("click", "li", setLanguage) .appendTo($p); var $footer = $("<div class='modal-footer' />") .appendTo($modal); var $cancel = $("<button class='dialog-button btn left'>") .on("click", function () { $modal.modal('hide'); }) .text(Strings.LANGUAGE_CANCEL) .appendTo($footer); $submit = $("<button class='dialog-button btn primary'>") .text(Strings.LANGUAGE_SUBMIT) .on("click", function () { if (!$activeLanguage) { return; } if (locale) { window.localStorage.setItem("locale", locale); } else { window.localStorage.removeItem("locale"); } CommandManager.execute(Commands.DEBUG_REFRESH_WINDOW); }) .attr("disabled", "disabled") .appendTo($footer); $modal .appendTo(window.document.body) .modal({ backdrop: "static", show: true }) .on("hidden", function () { $(this).remove(); }); // add system default var $li = $("<li>") .text("system default") .data("locale", null) .appendTo($ul); // add english $li = $("<li>") .text("en") .data("locale", "en") .appendTo($ul); // inspect all children of dirEntry entries.forEach(function (entry) { if (entry.isDirectory && entry.name.match(/^[a-z]{2}(-[A-Z]{2})?$/)) { var language = entry.name; var $li = $("<li>") .text(entry.name) .data("locale", language) .appendTo($ul); } }); }); }); } function _handleShowExtensionsFolder() { brackets.app.showExtensionsFolder( FileUtils.convertToNativePath(window.location.href), function (err) { // Ignore errors } ); } function _handleCheckForUpdates() { UpdateNotification.checkForUpdate(true); } /* Register all the command handlers */ // Show Developer Tools (optionally enabled) CommandManager.register(Strings.CMD_SHOW_DEV_TOOLS, Commands.DEBUG_SHOW_DEVELOPER_TOOLS, handleShowDeveloperTools) .setEnabled(!!brackets.app.showDeveloperTools); CommandManager.register(Strings.CMD_NEW_BRACKETS_WINDOW, Commands.DEBUG_NEW_BRACKETS_WINDOW, _handleNewBracketsWindow); CommandManager.register(Strings.CMD_SHOW_EXTENSIONS_FOLDER, Commands.DEBUG_SHOW_EXT_FOLDER, _handleShowExtensionsFolder); CommandManager.register(Strings.CMD_RUN_UNIT_TESTS, Commands.DEBUG_RUN_UNIT_TESTS, _handleRunUnitTests); CommandManager.register(Strings.CMD_SHOW_PERF_DATA, Commands.DEBUG_SHOW_PERF_DATA, _handleShowPerfData); CommandManager.register(Strings.CMD_SWITCH_LANGUAGE, Commands.DEBUG_SWITCH_LANGUAGE, _handleSwitchLanguage); CommandManager.register(Strings.CMD_USE_TAB_CHARS, Commands.TOGGLE_USE_TAB_CHARS, _handleUseTabChars) .setChecked(Editor.getUseTabChar()); CommandManager.register(Strings.CMD_CHECK_FOR_UPDATE, Commands.CHECK_FOR_UPDATE, _handleCheckForUpdates); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, window, $ */ define('view/ViewCommandHandlers',['require','exports','module','command/Commands','command/CommandManager','strings','project/ProjectManager','editor/EditorManager'],function (require, exports, module) { var Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), Strings = require("strings"), ProjectManager = require("project/ProjectManager"), EditorManager = require("editor/EditorManager"); /** * @const * @type {string} */ var DYNAMIC_FONT_STYLE_ID = "codemirror-dynamic-fonts"; function _removeDynamicFontSize(refresh) { $("#" + DYNAMIC_FONT_STYLE_ID).remove(); if (refresh) { EditorManager.getCurrentFullEditor().refreshAll(); } } /** * @private * Increases or decreases the editor's font size. * @param {number} -1 to make the font smaller; 1 to make it bigger. */ function _adjustFontSize(direction) { var styleId = "codemirror-dynamic-fonts"; var fsStyle = $(".CodeMirror-scroll").css("font-size"); var lhStyle = $(".CodeMirror-scroll").css("line-height"); var validFont = /^[\d\.]+(px|em)$/; // Make sure the font size and line height are expressed in terms // we can handle (px or em). If not, simply bail. if (fsStyle.search(validFont) === -1 || lhStyle.search(validFont) === -1) { return; } // Guaranteed to work by the validation above. var fsUnits = fsStyle.substring(fsStyle.length - 2, fsStyle.length); var lhUnits = lhStyle.substring(lhStyle.length - 2, lhStyle.length); var fsOld = parseFloat(fsStyle.substring(0, fsStyle.length - 2)); var lhOld = parseFloat(lhStyle.substring(0, lhStyle.length - 2)); var fsDelta = (fsUnits === "px") ? 1 : 0.1; var lhDelta = (lhUnits === "px") ? 1 : 0.1; if (direction === -1) { fsDelta *= -1; lhDelta *= -1; } var fsNew = fsOld + fsDelta; var lhNew = lhOld + lhDelta; var fsStr = fsNew + fsUnits; var lhStr = lhNew + lhUnits; // Don't let the fonts get too small. if (direction === -1 && ((fsUnits === "px" && fsNew <= 1) || (fsUnits === "em" && fsNew <= 0.1))) { return; } // It's necessary to inject a new rule to address all editors. _removeDynamicFontSize(false); var style = $("<style type='text/css'></style>").attr("id", DYNAMIC_FONT_STYLE_ID); style.html(".CodeMirror-scroll {" + "font-size: " + fsStr + " !important;" + "line-height: " + lhStr + " !important;}"); $("head").append(style); var editor = EditorManager.getCurrentFullEditor(); editor.refreshAll(); // Scroll the document back to its original position. This can only happen // if the font size is specified in pixels (which it currently is). if (fsUnits === "px") { var scrollPos = editor.getScrollPos(); var scrollDeltaX = Math.round(scrollPos.x / lhOld); var scrollDeltaY = Math.round(scrollPos.y / lhOld); editor.setScrollPos(scrollPos.x + (scrollDeltaX * direction), scrollPos.y + (scrollDeltaY * direction)); } } function _handleIncreaseFontSize() { _adjustFontSize(1); } function _handleDecreaseFontSize() { _adjustFontSize(-1); } function _handleRestoreFontSize() { _removeDynamicFontSize(true); } CommandManager.register(Strings.CMD_INCREASE_FONT_SIZE, Commands.VIEW_INCREASE_FONT_SIZE, _handleIncreaseFontSize); CommandManager.register(Strings.CMD_DECREASE_FONT_SIZE, Commands.VIEW_DECREASE_FONT_SIZE, _handleDecreaseFontSize); CommandManager.register(Strings.CMD_RESTORE_FONT_SIZE, Commands.VIEW_RESTORE_FONT_SIZE, _handleRestoreFontSize); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, PathUtils, window */ /* * Adds a "find in files" command to allow the user to find all occurances of a string in all files in * the project. * * The keyboard shortcut is Cmd(Ctrl)-Shift-F. * * FUTURE: * - Proper UI for both dialog and results * - Refactor dialog class and share with Quick File Open * - Search files in working set that are *not* in the project * - Handle matches that span mulitple lines * - Refactor UI from functionality to enable unit testing */ define('search/FindInFiles',['require','exports','module','utils/Async','command/CommandManager','command/Commands','strings','utils/StringUtils','document/DocumentManager','editor/EditorManager','project/FileIndexManager'],function (require, exports, module) { var Async = require("utils/Async"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), FileIndexManager = require("project/FileIndexManager"); // This dialog class was mostly copied from QuickOpen. We should have a common dialog // class that everyone can use. /** * FindInFilesDialog class * @constructor * */ function FindInFilesDialog() { this.closed = false; this.result = null; // $.Deferred } /** * Creates a dialog div floating on top of the current code mirror editor */ FindInFilesDialog.prototype._createDialogDiv = function (template) { this.dialog = $("<div />") .attr("class", "CodeMirror-dialog") .html("<div>" + template + "</div>") .prependTo($("#editor-holder")); }; /** * Closes the search dialog and resolves the promise that showDialog returned */ FindInFilesDialog.prototype._close = function (value) { if (this.closed) { return; } this.closed = true; this.dialog.remove(); EditorManager.focusEditor(); this.result.resolve(value); }; /** * Shows the search dialog * @param {?string} initialString Default text to prepopulate the search field with * @returns {$.Promise} that is resolved with the string to search for */ FindInFilesDialog.prototype.showDialog = function (initialString) { var dialogHTML = Strings.CMD_FIND_IN_FILES + ": <input type='text' id='findInFilesInput' style='width: 10em'> <span style='color: #888'>(" + Strings.SEARCH_REGEXP_INFO + ")</span>"; this.result = new $.Deferred(); this._createDialogDiv(dialogHTML); var $searchField = $("input#findInFilesInput"); var that = this; $searchField.attr("value", initialString || ""); $searchField.get(0).select(); $searchField.bind("keydown", function (event) { if (event.keyCode === 13 || event.keyCode === 27) { // Enter/Return key or Esc key event.stopPropagation(); event.preventDefault(); var query = $searchField.val(); if (event.keyCode === 27) { query = null; } that._close(query); } }) .blur(function () { that._close(null); }) .focus(); return this.result.promise(); }; function _getSearchMatches(contents, queryExpr) { // Quick exit if not found if (contents.search(queryExpr) === -1) { return null; } var trimmedContents = contents; var startPos = 0; var matchStart; var matches = []; var match; var lines = StringUtils.getLines(contents); while ((match = queryExpr.exec(contents)) !== null) { var lineNum = StringUtils.offsetToLineNum(lines, match.index); var line = lines[lineNum]; var ch = match.index - contents.lastIndexOf("\n", match.index) - 1; // 0-based index var matchLength = match[0].length; // Don't store more than 200 chars per line line = line.substr(0, Math.min(200, line.length)); matches.push({ start: {line: lineNum, ch: ch}, end: {line: lineNum, ch: ch + matchLength}, line: line }); } return matches; } function _showSearchResults(searchResults) { var $searchResultsDiv = $("#search-results"); if (searchResults && searchResults.length) { var $resultTable = $("<table class='zebra-striped condensed-table' />") .append("<tbody>"); // Count the total number of matches var numMatches = 0; searchResults.forEach(function (item) { numMatches += item.matches.length; }); // Show result summary in header $("#search-result-summary") .text("- " + numMatches + " match" + (numMatches > 1 ? "es" : "") + " in " + searchResults.length + " file" + (searchResults.length > 1 ? "s" : "") + (numMatches > 100 ? " (showing the first 100 matches)" : "")) .prepend(" "); // putting a normal space before the "-" is not enough var resultsDisplayed = 0; searchResults.forEach(function (item) { if (item && resultsDisplayed < 100) { var makeCell = function (content) { return $("<td/>").html(content); }; var esc = function (str) { str = str.replace(/</g, "<"); str = str.replace(/>/g, ">"); return str; }; var highlightMatch = function (line, start, end) { return esc(line.substr(0, start)) + "<span class='highlight'>" + esc(line.substring(start, end)) + "</span>" + esc(line.substr(end)); }; // Add row for file name $("<tr class='file-section' />") .append("<td colspan='3'>File: <b>" + item.fullPath + "</b></td>") .click(function () { // Clicking file section header collapses/expands result rows for that file var $fileHeader = $(this); $fileHeader.nextUntil(".file-section").toggle(); }) .appendTo($resultTable); // Add row for each match in file item.matches.forEach(function (match) { if (resultsDisplayed < 100) { var $row = $("<tr/>") .append(makeCell(" ")) // Indent .append(makeCell("line: " + (match.start.line + 1))) .append(makeCell(highlightMatch(match.line, match.start.ch, match.end.ch))) .appendTo($resultTable); $row.click(function () { CommandManager.execute(Commands.FILE_OPEN, {fullPath: item.fullPath}) .done(function (doc) { // Opened document is now the current main editor EditorManager.getCurrentFullEditor().setSelection(match.start, match.end); }); }); resultsDisplayed++; } }); } }); $("#search-results .table-container") .empty() .append($resultTable); $("#search-results .close") .one("click", function () { $searchResultsDiv.hide(); EditorManager.resizeEditor(); }); $searchResultsDiv.show(); } else { $searchResultsDiv.hide(); } EditorManager.resizeEditor(); } function _getQueryRegExp(query) { // If query is a regular expression, use it directly var isRE = query.match(/^\/(.*)\/(g|i)*$/); if (isRE) { // Make sure the 'g' flag is set var flags = isRE[2] || "g"; if (flags.search("g") === -1) { flags += "g"; } return new RegExp(isRE[1], flags); } // Query is a string. Turn it into a case-insensitive regexp // Escape regex special chars query = query.replace(/([(){}\[\].\^$|?+*\\])/g, "\\$1"); return new RegExp(query, "gi"); } /** * Displays a non-modal embedded dialog above the code mirror editor that allows the user to do * a find operation across all files in the project. */ function doFindInFiles() { var dialog = new FindInFilesDialog(); var searchResults = []; // Default to searching for the current selection var currentEditor = EditorManager.getFocusedEditor(); var initialString = currentEditor && currentEditor.getSelectedText(); dialog.showDialog(initialString) .done(function (query) { if (query) { var queryExpr = _getQueryRegExp(query); FileIndexManager.getFileInfoList("all") .done(function (fileListResult) { Async.doInParallel(fileListResult, function (fileInfo) { var result = new $.Deferred(); DocumentManager.getDocumentForPath(fileInfo.fullPath) .done(function (doc) { var matches = _getSearchMatches(doc.getText(), queryExpr); if (matches && matches.length) { searchResults.push({ fullPath: fileInfo.fullPath, matches: matches }); } result.resolve(); }) .fail(function (error) { // Error reading this file. This is most likely because the file isn't a text file. // Resolve here so we move on to the next file. result.resolve(); }); return result.promise(); }) .done(function () { _showSearchResults(searchResults); }) .fail(function () { console.log("find in files failed."); }); }); } }); } CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, doFindInFiles); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, doReplace */ /* * Adds Find and Replace commands * * Define search commands. Depends on dialog.js or another * implementation of the openDialog method. * * This code was copied from CodeMirror2/lib/util/search.js so that the UI strings * could be localized. * * Replace works a little oddly -- it will do the replace on the next findNext press. * You prevent a replace by making sure the match is no longer selected when hitting * findNext. * */ define('search/FindReplace',['require','exports','module','command/CommandManager','command/Commands','strings','editor/EditorManager'],function (require, exports, module) { var CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), EditorManager = require("editor/EditorManager"); function SearchState() { this.posFrom = this.posTo = this.query = null; this.marked = []; } function getSearchState(cm) { if (!cm._searchState) { cm._searchState = new SearchState(); } return cm._searchState; } function getSearchCursor(cm, query, pos) { // Heuristic: if the query string is all lowercase, do a case insensitive search. return cm.getSearchCursor(query, pos, typeof query === "string" && query === query.toLowerCase()); } function dialog(cm, text, shortText, f) { if (cm.openDialog) { cm.openDialog(text, f); } else { f(prompt(shortText, "")); } } function confirmDialog(cm, text, shortText, fs) { if (cm.openConfirm) { cm.openConfirm(text, fs); } else if (confirm(shortText)) { fs[0](); } } function parseQuery(query) { var isRE = query.match(/^\/(.*)\/([a-z]*)$/); return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") === -1 ? "" : "i") : query; } function findNext(cm, rev) { cm.operation(function () { var state = getSearchState(cm); var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo); if (!cursor.find(rev)) { cursor = getSearchCursor(cm, state.query, rev ? {line: cm.lineCount() - 1} : {line: 0, ch: 0}); if (!cursor.find(rev)) { return; } } cm.setSelection(cursor.from(), cursor.to()); state.posFrom = cursor.from(); state.posTo = cursor.to(); }); } var queryDialog = Strings.CMD_FIND + ': <input type="text" style="width: 10em"/> <span style="color: #888">(' + Strings.SEARCH_REGEXP_INFO + ')</span>'; function doSearch(cm, rev) { var state = getSearchState(cm); if (state.query) { return findNext(cm, rev); } dialog(cm, queryDialog, Strings.CMD_FIND, function (query) { cm.operation(function () { if (!query || state.query) { return; } state.query = parseQuery(query); if (cm.lineCount() < 2000) { // This is too expensive on big documents. var cursor = getSearchCursor(cm, query); while (cursor.findNext()) { state.marked.push(cm.markText(cursor.from(), cursor.to(), "CodeMirror-searching")); } } state.posFrom = state.posTo = cm.getCursor(); findNext(cm, rev); }); }); } function clearSearch(cm) { cm.operation(function () { var state = getSearchState(cm), i; if (!state.query) { return; } state.query = null; for (i = 0; i < state.marked.length; ++i) { state.marked[i].clear(); } state.marked.length = 0; }); } var replaceQueryDialog = Strings.CMD_REPLACE + ': <input type="text" style="width: 10em"/> <span style="color: #888">(' + Strings.SEARCH_REGEXP_INFO + ')</span>'; var replacementQueryDialog = Strings.WITH + ': <input type="text" style="width: 10em"/>'; // style buttons to match height/margins/border-radius of text input boxes var style = ' style="padding:5px 15px;border:1px #999 solid;border-radius:3px;margin:2px 2px 5px;"'; var doReplaceConfirm = Strings.CMD_REPLACE + '? <button' + style + '>' + Strings.BUTTON_YES + '</button> <button' + style + '>' + Strings.BUTTON_NO + '</button> <button' + style + '>' + Strings.BUTTON_STOP + '</button>'; function replace(cm, all) { dialog(cm, replaceQueryDialog, Strings.CMD_REPLACE, function (query) { if (!query) { return; } query = parseQuery(query); dialog(cm, replacementQueryDialog, Strings.WITH, function (text) { var match, fnMatch = function (w, i) { return match[i]; }; if (all) { cm.compoundChange(function () { cm.operation(function () { var cursor = getSearchCursor(cm, query); while (cursor.findNext()) { if (typeof query !== "string") { match = cm.getRange(cursor.from(), cursor.to()).match(query); cursor.replace(text.replace(/\$(\d)/, fnMatch)); } else { cursor.replace(text); } } }); }); } else { clearSearch(cm); var cursor = getSearchCursor(cm, query, cm.getCursor()); var advance = function () { var start = cursor.from(), match = cursor.findNext(); if (!match) { cursor = getSearchCursor(cm, query); match = cursor.findNext(); if (!match || (start && cursor.from().line === start.line && cursor.from().ch === start.ch)) { return; } } cm.setSelection(cursor.from(), cursor.to()); confirmDialog(cm, doReplaceConfirm, Strings.CMD_REPLACE + "?", [function () { doReplace(match); }, advance]); }; var doReplace = function (match) { cursor.replace(typeof query === "string" ? text : text.replace(/\$(\d)/, fnMatch)); advance(); }; advance(); } }); }); } function _launchFind() { var editor = EditorManager.getFocusedEditor(); if (editor) { var codeMirror = editor._codeMirror; // Bring up CodeMirror's existing search bar UI clearSearch(codeMirror); doSearch(codeMirror); // Prepopulate the search field with the current selection, if any $(".CodeMirror-dialog input[type='text']") .attr("value", codeMirror.getSelection()) .get(0).select(); } } function _findNext() { var editor = EditorManager.getFocusedEditor(); if (editor) { doSearch(editor._codeMirror); } } function _findPrevious() { var editor = EditorManager.getFocusedEditor(); if (editor) { doSearch(editor._codeMirror, true); } } function _replace() { var editor = EditorManager.getFocusedEditor(); if (editor) { replace(editor._codeMirror); } } CommandManager.register(Strings.CMD_FIND, Commands.EDIT_FIND, _launchFind); CommandManager.register(Strings.CMD_FIND_NEXT, Commands.EDIT_FIND_NEXT, _findNext); CommandManager.register(Strings.CMD_REPLACE, Commands.EDIT_REPLACE, _replace); CommandManager.register(Strings.CMD_FIND_PREVIOUS, Commands.EDIT_FIND_PREVIOUS, _findPrevious); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * ExtensionUtils defines utility methods for implementing extensions. */ define('utils/ExtensionUtils',['require','exports','module'],function (require, exports, module) { /** * Loads a style sheet relative to the extension module. * * @param {!module} module Module provided by RequireJS * @param {!string} path Relative path from the extension folder to a CSS file * @return {!$.Promise} A promise object that is resolved if the CSS file can be loaded. */ function loadStyleSheet(module, path) { var modulePath = module.uri.substr(0, module.uri.lastIndexOf("/") + 1), url = encodeURI(modulePath + path), result = new $.Deferred(); // Make a request for the same file in order to record success or failure. // The link element's onload and onerror events are not consistently supported. $.get(url).done(function () { var $link = $("<link/>"); $link.attr({ type: "text/css", rel: "stylesheet", href: url }); $("head").append($link[0]); result.resolve($link[0]); }).fail(function (err) { result.reject(err); }); return result; } exports.loadStyleSheet = loadStyleSheet; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ /** * This is JavaScript API exposed to the native shell when Brackets is run in a native shell rather than a browser. */ define('utils/ShellAPI',['require','exports','module','command/CommandManager'],function (require, exports, module) { // Load dependent modules var CommandManager = require("command/CommandManager"); /** * The native function BracketsShellAPI::DispatchBracketsJSCommand calls this function in order to enable * calling Brackets commands from the native shell. */ function executeCommand(eventName) { var evt = window.document.createEvent("Event"); evt.initEvent(eventName, false, true); CommandManager.execute(eventName, {evt: evt}); //return if default was prevented return evt.defaultPrevented; } exports.executeCommand = executeCommand; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global require, define, brackets: true, $, PathUtils, window, navigator, Mustache */ require.config({ paths: { "text" : "thirdparty/text", "i18n" : "thirdparty/i18n" }, // Use custom brackets property until CEF sets the correct navigator.language // NOTE: When we change to navigator.language here, we also should change to // navigator.language in ExtensionLoader (when making require contexts for each // extension). locale: window.localStorage.getItem("locale") || brackets.app.language }); /** * brackets is the root of the Brackets codebase. This file pulls in all other modules as * dependencies (or dependencies thereof), initializes the UI, and binds global menus & keyboard * shortcuts to their Commands. * * TODO: (issue #264) break out the definition of brackets into a separate module from the application controller logic * * Unlike other modules, this one can be accessed without an explicit require() because it exposes * a global object, window.brackets. * * Events: * htmlContentLoadComplete - sent when the HTML DOM is fully loaded. Modules should not touch * or modify DOM elements before this event is sent. */ define('brackets',['require','exports','module','widgets/bootstrap-dropdown','widgets/bootstrap-modal','thirdparty/path-utils/path-utils.min','thirdparty/smart-auto-complete/jquery.smart_autocomplete','LiveDevelopment/main','project/ProjectManager','document/DocumentManager','editor/EditorManager','editor/CSSInlineEditor','language/JSUtils','project/WorkingSetView','document/DocumentCommandHandlers','project/FileViewController','project/FileSyncManager','command/KeyBindingManager','command/Commands','command/CommandManager','utils/BuildInfoUtils','editor/CodeHintManager','language/JSLintUtils','utils/PerfUtils','project/FileIndexManager','search/QuickOpen','command/Menus','file/FileUtils','text!htmlContent/main-view.html','strings','widgets/Dialogs','utils/ExtensionLoader','project/SidebarView','utils/Async','utils/UpdateNotification','utils/UrlParams','document/ChangedDocumentTracker','editor/EditorCommandHandlers','debug/DebugCommandHandlers','view/ViewCommandHandlers','search/FindInFiles','search/FindReplace','utils/ExtensionUtils','utils/ShellAPI','preferences/PreferencesManager','command/CommandManager','language/CSSUtils','LiveDevelopment/LiveDevelopment','LiveDevelopment/Inspector/Inspector','utils/NativeApp','utils/ExtensionUtils','utils/UpdateNotification'],function (require, exports, module) { // Load dependent non-module scripts require("widgets/bootstrap-dropdown"); require("widgets/bootstrap-modal"); require("thirdparty/path-utils/path-utils.min"); require("thirdparty/smart-auto-complete/jquery.smart_autocomplete"); // Load LiveDeveopment require("LiveDevelopment/main"); // Load dependent modules var ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), CSSInlineEditor = require("editor/CSSInlineEditor"), JSUtils = require("language/JSUtils"), WorkingSetView = require("project/WorkingSetView"), DocumentCommandHandlers = require("document/DocumentCommandHandlers"), FileViewController = require("project/FileViewController"), FileSyncManager = require("project/FileSyncManager"), KeyBindingManager = require("command/KeyBindingManager"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), BuildInfoUtils = require("utils/BuildInfoUtils"), CodeHintManager = require("editor/CodeHintManager"), JSLintUtils = require("language/JSLintUtils"), PerfUtils = require("utils/PerfUtils"), FileIndexManager = require("project/FileIndexManager"), QuickOpen = require("search/QuickOpen"), Menus = require("command/Menus"), FileUtils = require("file/FileUtils"), MainViewHTML = require("text!htmlContent/main-view.html"), Strings = require("strings"), Dialogs = require("widgets/Dialogs"), ExtensionLoader = require("utils/ExtensionLoader"), SidebarView = require("project/SidebarView"), Async = require("utils/Async"), UpdateNotification = require("utils/UpdateNotification"), UrlParams = require("utils/UrlParams").UrlParams; // Local variables var bracketsReady = false, bracketsReadyHandlers = [], params = new UrlParams(); // read URL params params.parse(); //Load modules that self-register and just need to get included in the main project require("document/ChangedDocumentTracker"); require("editor/EditorCommandHandlers"); require("debug/DebugCommandHandlers"); require("view/ViewCommandHandlers"); require("search/FindInFiles"); require("search/FindReplace"); require("utils/ExtensionUtils"); function _callBracketsReadyHandler(handler) { try { handler(); } catch (e) { console.log("Exception when calling a 'brackets done loading' handler"); console.log(e); } } function _onBracketsReady() { var i; bracketsReady = true; for (i = 0; i < bracketsReadyHandlers.length; i++) { _callBracketsReadyHandler(bracketsReadyHandlers[i]); } bracketsReadyHandlers = []; } // WARNING: This event won't fire if ANY extension fails to load or throws an error during init. // To fix this, we need to make a change to _initExtensions (filed as issue 1029) function _registerBracketsReadyHandler(handler) { if (bracketsReady) { _callBracketsReadyHandler(handler); } else { bracketsReadyHandlers.push(handler); } } // TODO: Issue 949 - the following code should be shared function _initGlobalBrackets() { // Define core brackets namespace if it isn't already defined // // We can't simply do 'brackets = {}' to define it in the global namespace because // we're in "use strict" mode. Most likely, 'window' will always point to the global // object when this code is running. However, in case it isn't (e.g. if we're running // inside Node for CI testing) we use this trick to get the global object. // // Taken from: // http://stackoverflow.com/questions/3277182/how-to-get-the-global-object-in-javascript var Fn = Function, global = (new Fn("return this"))(); if (!global.brackets) { global.brackets = {}; } // Uncomment the following line to force all low level file i/o routines to complete // asynchronously. This should only be done for testing/debugging. // NOTE: Make sure this line is commented out again before committing! //brackets.forceAsyncCallbacks = true; // Load native shell when brackets is run in a native shell rather than the browser // TODO: (issue #266) load conditionally brackets.shellAPI = require("utils/ShellAPI"); brackets.inBrowser = !brackets.hasOwnProperty("fs"); brackets.platform = (global.navigator.platform === "MacIntel" || global.navigator.platform === "MacPPC") ? "mac" : "win"; // Loading extensions requires creating new require.js contexts, which requires access to the global 'require' object // that always gets hidden by the 'require' in the AMD wrapper. We store this in the brackets object here so that // the ExtensionLoader doesn't have to have access to the global object. brackets.libRequire = global.require; // Also store our current require.js context (the one that loads brackets core modules) so that extensions can use it // Note: we change the name to "getModule" because this won't do exactly the same thing as 'require' in AMD-wrapped // modules. The extension will only be able to load modules that have already been loaded once. brackets.getModule = require; // Provide a way for anyone (including code not using require) to register a handler for the brackets 'ready' event // This event is like $(document).ready in that it will call the handler immediately if brackets is already done loading // // WARNING: This event won't fire if ANY extension fails to load or throws an error during init. // To fix this, we need to make a change to _initExtensions (filed as issue 1029) // // TODO (issue 1034): We *could* use a $.Deferred for this, except deferred objects enter a broken // state if any resolution callback throws an exception. Since third parties (e.g. extensions) may // add callbacks to this, we need to be robust to exceptions brackets.ready = _registerBracketsReadyHandler; } // TODO: (issue 1029) Add timeout to main extension loading promise, so that we always call this function // Making this fix will fix a warning (search for issue 1029) related to the brackets 'ready' event. function _initExtensions() { // allow unit tests to override which plugin folder(s) to load var paths = params.get("extensions") || "default,user"; return Async.doInParallel(paths.split(","), function (item) { return ExtensionLoader.loadAllExtensionsInNativeDirectory( FileUtils.getNativeBracketsDirectoryPath() + "/extensions/" + item, "extensions/" + item ); }); } function _initTest() { // TODO: (issue #265) Make sure the "test" object is not included in final builds // All modules that need to be tested from the context of the application // must to be added to this object. The unit tests cannot just pull // in the modules since they would run in context of the unit test window, // and would not have access to the app html/css. brackets.test = { PreferencesManager : require("preferences/PreferencesManager"), ProjectManager : ProjectManager, DocumentCommandHandlers : DocumentCommandHandlers, FileViewController : FileViewController, DocumentManager : DocumentManager, EditorManager : EditorManager, Commands : Commands, WorkingSetView : WorkingSetView, JSLintUtils : JSLintUtils, PerfUtils : PerfUtils, JSUtils : JSUtils, CommandManager : require("command/CommandManager"), FileSyncManager : FileSyncManager, FileIndexManager : FileIndexManager, Menus : Menus, KeyBindingManager : KeyBindingManager, CodeHintManager : CodeHintManager, CSSUtils : require("language/CSSUtils"), LiveDevelopment : require("LiveDevelopment/LiveDevelopment"), Inspector : require("LiveDevelopment/Inspector/Inspector"), NativeApp : require("utils/NativeApp"), ExtensionUtils : require("utils/ExtensionUtils"), UpdateNotification : require("utils/UpdateNotification"), doneLoading : false }; brackets.ready(function () { brackets.test.doneLoading = true; }); } function _initDragAndDropListeners() { // Prevent unhandled drag and drop of files into the browser from replacing // the entire Brackets app. This doesn't prevent children from choosing to // handle drops. $(window.document.body) .on("dragover", function (event) { if (event.originalEvent.dataTransfer.files) { event.stopPropagation(); event.preventDefault(); event.originalEvent.dataTransfer.dropEffect = "none"; } }) .on("drop", function (event) { if (event.originalEvent.dataTransfer.files) { event.stopPropagation(); event.preventDefault(); } }); } function _initCommandHandlers() { // Most command handlers are automatically registered when their module is loaded (see "modules // that self-register" above for some). A few commands need an extra kick here though: DocumentCommandHandlers.init($("#main-toolbar")); // About dialog CommandManager.register(Strings.CMD_ABOUT, Commands.HELP_ABOUT, function () { // If we've successfully determined a "build number" via .git metadata, add it to dialog var bracketsSHA = BuildInfoUtils.getBracketsSHA(), bracketsAppSHA = BuildInfoUtils.getBracketsAppSHA(), versionLabel = ""; if (bracketsSHA) { versionLabel += " (" + bracketsSHA.substr(0, 7) + ")"; } if (bracketsAppSHA) { versionLabel += " (shell " + bracketsAppSHA.substr(0, 7) + ")"; } $("#about-build-number").text(versionLabel); Dialogs.showModalDialog(Dialogs.DIALOG_ID_ABOUT); }); } function _initWindowListeners() { // TODO: (issue 269) to support IE, need to listen to document instead (and even then it may not work when focus is in an input field?) $(window).focus(function () { FileSyncManager.syncOpenDocuments(); FileIndexManager.markDirty(); }); } function _onReady() { // Add the platform (mac or win) to the body tag so we can have platform-specific CSS rules $("body").addClass("platform-" + brackets.platform); EditorManager.setEditorHolder($("#editor-holder")); // Let the user know Brackets doesn't run in a web browser yet if (brackets.inBrowser) { Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_BRACKETS_IN_BROWSER_TITLE, Strings.ERROR_BRACKETS_IN_BROWSER ); } _initDragAndDropListeners(); _initCommandHandlers(); KeyBindingManager.init(); Menus.init(); // key bindings should be initialized first _initWindowListeners(); // Read "build number" SHAs off disk at the time the matching Brackets JS code is being loaded, instead // of later, when they may have been updated to a different version BuildInfoUtils.init(); // Use quiet scrollbars if we aren't on Lion. If we're on Lion, only // use native scroll bars when the mouse is not plugged in or when // using the "Always" scroll bar setting. var osxMatch = /Mac OS X 10\D([\d+])\D/.exec(navigator.userAgent); if (osxMatch && osxMatch[1] && Number(osxMatch[1]) >= 7) { // test a scrolling div for scrollbars var $testDiv = $("<div style='position:fixed;left:-50px;width:50px;height:50px;overflow:auto;'><div style='width:100px;height:100px;'/></div>").appendTo(window.document.body); if ($testDiv.outerWidth() === $testDiv.get(0).clientWidth) { $(".sidebar").removeClass("quiet-scrollbars"); } $testDiv.remove(); } PerfUtils.addMeasurement("Application Startup"); // finish UI initialization before loading extensions var initialProjectPath = ProjectManager.getInitialProjectPath(); ProjectManager.openProject(initialProjectPath).done(function () { _initTest(); _initExtensions().always(_onBracketsReady); }); // Check for updates if (!params.get("skipUpdateCheck")) { UpdateNotification.checkForUpdate(); } } // Main Brackets initialization _initGlobalBrackets(); // Localize MainViewHTML and inject into <BODY> tag $('body').html(Mustache.render(MainViewHTML, Strings)); // modules that depend on the HTML DOM should listen to // the htmlContentLoadComplete event. $(brackets).trigger("htmlContentLoadComplete"); $(window.document).ready(_onReady); }); /* ============================================================ * bootstrap-dropdown.js v1.4.0 * http://twitter.github.com/bootstrap/javascript.html#dropdown * ============================================================ * Copyright 2011 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ============================================================ */ !function( $ ){ "use strict" /* DROPDOWN PLUGIN DEFINITION * ========================== */ $.fn.dropdown = function ( selector ) { return this.each(function () { $(this).delegate(selector || d, 'click', function (e) { var li = $(this).parent('li') , isActive = li.hasClass('open') clearMenus() !isActive && li.toggleClass('open') return false }) }) } /* APPLY TO STANDARD DROPDOWN ELEMENTS * =================================== */ var d = 'a.menu, .dropdown-toggle' function clearMenus() { $(d).parent('li').removeClass('open') } $(function () { $('html').bind("click", clearMenus) $('body').dropdown( '[data-dropdown] a.menu, [data-dropdown] .dropdown-toggle' ) }) }( window.jQuery || window.ender ); define("widgets/bootstrap-dropdown", function(){}); /* ========================================================= * bootstrap-modal.js v1.4.0 * http://twitter.github.com/bootstrap/javascript.html#modal * ========================================================= * Copyright 2011 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ========================================================= */ !function( $ ){ "use strict" /* CSS TRANSITION SUPPORT (https://gist.github.com/373874) * ======================================================= */ var transitionEnd $(document).ready(function () { $.support.transition = (function () { var thisBody = document.body || document.documentElement , thisStyle = thisBody.style , support = thisStyle.transition !== undefined || thisStyle.WebkitTransition !== undefined || thisStyle.MozTransition !== undefined || thisStyle.MsTransition !== undefined || thisStyle.OTransition !== undefined return support })() // set CSS transition event type if ( $.support.transition ) { transitionEnd = "TransitionEnd" if ( $.browser.webkit ) { transitionEnd = "webkitTransitionEnd" } else if ( $.browser.mozilla ) { transitionEnd = "transitionend" } else if ( $.browser.opera ) { transitionEnd = "oTransitionEnd" } } }) /* MODAL PUBLIC CLASS DEFINITION * ============================= */ var Modal = function ( content, options ) { this.settings = $.extend({}, $.fn.modal.defaults, options) this.$element = $(content) .delegate('.close', 'click.modal', $.proxy(this.hide, this)) if ( this.settings.show ) { this.show() } return this } Modal.prototype = { toggle: function () { return this[!this.isShown ? 'show' : 'hide']() } , show: function () { var that = this this.isShown = true this.$element.trigger('show') escape.call(this) backdrop.call(this, function () { var transition = $.support.transition && that.$element.hasClass('fade') that.$element .appendTo(document.body) .show() if (transition) { that.$element[0].offsetWidth // force reflow } that.$element.addClass('in') transition ? that.$element.one(transitionEnd, function () { that.$element.trigger('shown') }) : that.$element.trigger('shown') }) return this } , hide: function (e) { e && e.preventDefault() if ( !this.isShown ) { return this } var that = this this.isShown = false escape.call(this) this.$element .trigger('hide') .removeClass('in') $.support.transition && this.$element.hasClass('fade') ? hideWithTransition.call(this) : hideModal.call(this) return this } } /* MODAL PRIVATE METHODS * ===================== */ function hideWithTransition() { // firefox drops transitionEnd events :{o var that = this , timeout = setTimeout(function () { that.$element.unbind(transitionEnd) hideModal.call(that) }, 500) this.$element.one(transitionEnd, function () { clearTimeout(timeout) hideModal.call(that) }) } function hideModal (that) { this.$element .hide() .trigger('hidden') backdrop.call(this) } function backdrop ( callback ) { var that = this , animate = this.$element.hasClass('fade') ? 'fade' : '' if ( this.isShown && this.settings.backdrop ) { var doAnimate = $.support.transition && animate this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') .appendTo(document.body) if ( this.settings.backdrop != 'static' ) { this.$backdrop.click($.proxy(this.hide, this)) } if ( doAnimate ) { this.$backdrop[0].offsetWidth // force reflow } this.$backdrop.addClass('in') doAnimate ? this.$backdrop.one(transitionEnd, callback) : callback() } else if ( !this.isShown && this.$backdrop ) { this.$backdrop.removeClass('in') $.support.transition && this.$element.hasClass('fade')? this.$backdrop.one(transitionEnd, $.proxy(removeBackdrop, this)) : removeBackdrop.call(this) } else if ( callback ) { callback() } } function removeBackdrop() { this.$backdrop.remove() this.$backdrop = null } function escape() { var that = this if ( this.isShown && this.settings.keyboard ) { $(document).bind('keyup.modal', function ( e ) { if ( e.which == 27 ) { that.hide() } }) } else if ( !this.isShown ) { $(document).unbind('keyup.modal') } } /* MODAL PLUGIN DEFINITION * ======================= */ $.fn.modal = function ( options ) { var modal = this.data('modal') if (!modal) { if (typeof options == 'string') { options = { show: /show|toggle/.test(options) } } return this.each(function () { $(this).data('modal', new Modal(this, options)) }) } if ( options === true ) { return modal } if ( typeof options == 'string' ) { modal[options]() } else if ( modal ) { modal.toggle() } return this } $.fn.modal.Modal = Modal $.fn.modal.defaults = { backdrop: false , keyboard: false , show: false } /* MODAL DATA- IMPLEMENTATION * ========================== */ $(document).ready(function () { $('body').delegate('[data-controls-modal]', 'click', function (e) { e.preventDefault() var $this = $(this).data('show', true) $('#' + $this.attr('data-controls-modal')).modal( $this.data() ) }) }) }( window.jQuery || window.ender ); define("widgets/bootstrap-modal", function(){}); // path-utils.js - version 0.1 // Copyright (c) 2011, Kin Blas // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of the <organization> nor the // names of its contributors may be used to endorse or promote products // derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY // DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND // ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. (function(l){function m(a){return function(e){return b.parseUrl(e)[a]}}var b={urlParseRE:/^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#\.]*(?:\.[^\?#\.]+)*(\.[^\?#\.]+)|[^\?#]*)))?(\?[^#]+)?)(#.*)?/,parsedUrlPropNames:"href,hrefNoHash,hrefNoSearch,domain,protocol,doubleSlash,authority,userinfo,username,password,host,hostname,port,pathname,directory,filename,filenameExtension,search,hash".split(","), defaultPorts:{http:"80",https:"443",ftp:"20",ftps:"990"},parseUrl:function(a){if(a&&typeof a==="object")return a;var a=b.urlParseRE.exec(a||"")||[],e=b.parsedUrlPropNames,c=e.length,f={},d;for(d=0;d<c;d++)f[e[d]]=a[d]||"";return f},port:function(a){a=b.parseUrl(a);return a.port||b.defaultPorts[a.protocol]},isSameDomain:function(a,e){return b.parseUrl(a).domain===b.parseUrl(e).domain},isRelativeUrl:function(a){return b.parseUrl(a).protocol===""},isAbsoluteUrl:function(a){return b.parseUrl(a).protocol!== ""},makePathAbsolute:function(a,e){if(a&&a.charAt(0)==="/")return a;for(var a=a||"",c=(e=e?e.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"")?e.split("/"):[],b=a.split("/"),d=0;d<b.length;d++){var j=b[d];switch(j){case ".":break;case "..":c.length&&c.pop();break;default:c.push(j)}}return"/"+c.join("/")},makePathRelative:function(a,b){for(var b=b?b.replace(/^\/|\/?[^\/]*$/g,""):"",a=a?a.replace(/^\//,""):"",c=b?b.split("/"):[],f=a.split("/"),d=[],j=c.length,g=false,i=0;i<j;i++)(g=g||f[0]!==c[i])?d.push(".."): f.shift();return d.concat(f).join("/")},makeUrlAbsolute:function(a,e){if(!b.isRelativeUrl(a))return a;var c=b.parseUrl(a),f=b.parseUrl(e),d=c.protocol||f.protocol,g=c.protocol?c.doubleSlash:c.doubleSlash||f.doubleSlash,h=c.authority||f.authority,i=c.pathname!=="",k=b.makePathAbsolute(c.pathname||f.filename,f.pathname);return d+g+h+k+(c.search||!i&&f.search||"")+c.hash}},g,h,k=b.parsedUrlPropNames,n=k.length;for(g=0;g<n;g++)h=k[g],b[h]||(b[h]=m(h));l.PathUtils=b})(window); define("thirdparty/path-utils/path-utils.min", function(){}); /** * Smart Auto Complete plugin * * Copyright (c) 2011 Lakshan Perera (laktek.com) * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) licenses. * */ /* Requirements: jQuery 1.5 or above Usage: $(target).smartAutoComplete({options}) Options: minCharLimit: (integer) minimum characters user have to type before invoking the autocomplete (default: 1) maxCharLimit: (integer) maximum characters user can type while invoking the autocomplete (default: null (unlimited)) maxResults: (integer) maximum number of results to return (default: null (unlimited)) delay: (integer) delay before autocomplete starts (default: 0) disabled: (boolean) whether autocomplete disabled on the field (default: false) forceSelect: (boolean) If set to true, field will be always filled with best matching result, without leaving the custom input. Better to enable this option, if you want autocomplete field to behave similar to a HTML select field. (Check Example 2 in the demo) (default: false) typeAhead: (boolean) If set to true, it will offer the best matching result in grey within the field; that can be auto-completed by pressing the right arrow-key or enter. This is similar to behaviour in Google Instant Search's query field (Check Example 3 in the demo) (default: false) source: (string/array) you can supply an array with items or a string containing a URL to fetch items for the source this is optional if you prefer to have your own filter method filter: (function) define a custom function that would return matching items to the entered text (this will override the default filtering algorithm) should return an array or a Deferred object (ajax call) parameters available: term, source resultFormatter: (function) the function you supply here will be called to format the output of an individual result. should return a string parameters available: result resultsContainer: (selector) to which element(s) the result should be appended. resultElement: (selector) references to the result elements collection (e.g. li, div.result) Events: keyIn: fires when user types into the field (parameters: query) resultsReady: fires when the filter function returns (parameters: results) showResults: fires when results are shown (parameters: results) noResults: fires when filter returns an empty array itemSelect: fires when user selects an item from the result list (paramters: item) itemFocus: fires when user highlights an item with mouse or arrow keys (paramters: item) itemUnfocus: fires when user moves out from an highlighted item (paramters: item) lostFocus: fires when autocomplete field loses focus by user clicking outside of the field or focusing on another field. Also, this event is fired when a value is selected }) */ (function($){ $.fn.smartAutoComplete = function(){ if(arguments.length < 1){ // get the smart autocomplete object of the first element and return var first_element = this[0]; return $(first_element).data("smart-autocomplete") } var default_filter_matcher = function(term, source, context){ var matcher = new RegExp(term.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), "i" ); return $.grep(source, function(value) { return matcher.test( value ); }); } var default_options = { minCharLimit: 1, maxCharLimit: null, maxResults: null, delay: 0, disabled: false, forceSelect: false, typeAhead: false, resultElement: "li", resultFormatter: function(r){ return ("<li>" + r + "</li>"); }, filter: function(term, source){ var context = this; var options = $(context).data('smart-autocomplete'); //when source is an array if($.type(source) === "array") { // directly map var results = default_filter_matcher(term, source, context); return results; } //when source is a string else if($.type(source) === "string"){ // treat the string as a URL endpoint // pass the query as 'term' return $.Deferred(function(dfd){ $.ajax({ url: source, data: {"term": term}, dataType: "json" }).success( function(data){ dfd.resolve( default_filter_matcher(term, data, context) ); }); }).promise(); } }, alignResultsContainer: false, clearResults: function(){ //remove type ahead field var type_ahead_field = $(this.context).prev(".smart_autocomplete_type_ahead_field"); $(this.context).css({ background: type_ahead_field.css("background") }); type_ahead_field.remove(); //clear results div $(this.resultsContainer).html(""); }, setCurrentSelectionToContext: function(){ if(this.rawResults.length > 0 && this.currentSelection >= 0) $(this.context).val(this.rawResults[(this.currentSelection)]); }, setItemSelected: function(val){ this.itemSelected = val; }, autocompleteFocused: false, setAutocompleteFocused: function(val){ this.autocompleteFocused = val; } }; //define the default events $.event.special.keyIn = { setup: function(){ return false; }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var source = options.source || null; var filter = options.filter; var maxChars = (options.maxCharLimit > 0 ? options.maxCharLimit : Number.POSITIVE_INFINITY) //event specific data var query = ev.smartAutocompleteData.query; if(options.disabled || (query.length > maxChars)){ return false; } //set item selected property options.setItemSelected(false); //set autocomplete focused options.setAutocompleteFocused(true); //call the filter function with delay setTimeout(function(){ $.when( filter.apply(options, [query, options.source]) ).done(function( results ){ //do the trimming var trimmed_results = (options.maxResults > 0 ? results.splice(0, options.maxResults) : results); $(context).trigger('resultsReady', [trimmed_results]); }); }, options.delay); } }; $.event.special.resultsReady = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //event specific data var results = ev.smartAutocompleteData.results; //exit if smart complete is disabled if(options.disabled) return false; //clear all previous results $(context).smartAutoComplete().clearResults(); //save the raw results options.rawResults = results; //fire the no match event and exit if no matching results if(results.length < 1){ $(context).trigger('noResults'); return false } //call the results formatter function var formatted_results = $.map(results, function(result){ return options.resultFormatter.apply(options, [result]); }); var formatted_results_html = formatted_results.join(""); //append the results to the container if(options.resultsContainer) $(options.resultsContainer).append(formatted_results_html); //trigger results ready event $(context).trigger('showResults', [results]); } }; $.event.special.showResults = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var results_container = $(options.resultsContainer); //event specific data var raw_results = ev.smartAutocompleteData.results; //type ahead if(options.typeAhead && (raw_results[0].substr(0, $(context).val().length) == $(context).val()) ){ var suggestion = raw_results[0]; //options.typeAheadExtractor($(context).val(), raw_results[0]); //add new typeAhead field $(context).before("<input class='smart_autocomplete_type_ahead_field' type='text' autocomplete='off' disabled='disabled' value='" + suggestion + "'/>"); $(context).css({ position: "relative", zIndex: 2, background: 'transparent' }); var typeAheadField = $(context).prev("input"); typeAheadField.css({ position: "absolute", zIndex: 1, overflow: 'hidden', background: $(context).css("background"), borderColor: 'transparent', width: $(context).width(), color: 'silver' }); //trigger item over for first item options.currentSelection = 0; if(results_container) $(context).trigger('itemFocus', results_container.children()[options.currentSelection]); } //show the results container after aligning it with the field if(results_container){ if(options.alignResultsContainer){ results_container.css({ position: "absolute", top: function(){ return $(context).offset().top + $(context).height(); }, left: function(){ return $(context).offset().left; }, width: function(){ return $(context).width(); }, zIndex: 1000 }) } results_container.show(); } } }; $.event.special.noResults = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); var result_container = $(options.resultsContainer); if(result_container){ //clear previous results options.clearResults(); } } }; $.event.special.itemSelect = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //event specific data var selected_item = ev.smartAutocompleteData.item; //get the text from selected item var selected_value = $(selected_item).text() || $(selected_item).val(); //set it as the value of the autocomplete field $(context).val(selected_value); //set item selected property options.setItemSelected(true); //set number of current chars in field options.originalCharCount = $(context).val().length; //trigger lost focus $(context).trigger('lostFocus'); } }; $.event.special.itemFocus = { setup: function(){ return false }, _default: function(ev){ //event specific data var item = ev.smartAutocompleteData.item; $(item).addClass("smart_autocomplete_highlight"); } }; $.event.special.itemUnfocus = { setup: function(){ return false }, _default: function(ev){ //event specific data var item = ev.smartAutocompleteData.item; $(item).removeClass("smart_autocomplete_highlight"); } } $.event.special.lostFocus = { setup: function(){ return false }, _default: function(ev){ var context = ev.target; var options = $(context).data("smart-autocomplete"); //if force select is selected and no item is selected, clear currently entered text if(options.forceSelect && !options.itemSelected) $(options.context).val(""); //unset autocomplete focused options.setAutocompleteFocused(false); //clear results options.clearResults(); //hide the results container if(options.resultsContainer) $(options.resultsContainer).hide(); //set current selection to null options.currentSelection = null; } }; var passed_options = arguments[0]; return this.each(function(i) { //set the options var options = $.extend(default_options, $(this).data("smart-autocomplete"), passed_options); //set the context options['context'] = this; //if a result container is not defined if($.type(options.resultsContainer) === 'undefined' ){ //define the default result container if it is already not defined var default_container = $("<ul class='smart_autocomplete_container' style='display:none'></ul>"); default_container.appendTo("body"); options.resultsContainer = default_container; options.alignResultsContainer = true; } $(this).data("smart-autocomplete", options); // bind user events $(this).keyup(function(ev){ //get the options var options = $(this).data("smart-autocomplete"); //up arrow if(ev.keyCode == '38'){ if(options.resultsContainer){ var current_selection = options.currentSelection || 0; var result_suggestions = $(options.resultsContainer).children(); if(current_selection >= 0) $(options.context).trigger('itemUnfocus', result_suggestions[current_selection] ); if(--current_selection <= 0) current_selection = 0; options['currentSelection'] = current_selection; $(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] ); } } //down arrow else if(ev.keyCode == '40'){ if(options.resultsContainer && options.resultsContainer.is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); if(current_selection >= 0) $(options.context).trigger('itemUnfocus', result_suggestions[current_selection] ); if(isNaN(current_selection) || null == current_selection || (++current_selection >= result_suggestions.length) ) current_selection = 0; options['currentSelection'] = current_selection; $(options.context).trigger('itemFocus', [ result_suggestions[current_selection] ] ); } //trigger keyIn event on down key else { $(options.context).trigger('keyIn', [$(this).val()]); } } //right arrow & enter key else if(ev.keyCode == '39' || ev.keyCode == '13'){ var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field'); if(options.resultsContainer && $(options.resultsContainer).is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); $(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] ); } else if(options.typeAhead && type_ahead_field.is(':visible')) $(options.context).trigger('itemSelect', [ type_ahead_field ] ); return false; } else { var current_char_count = $(options.context).val().length; //check whether the string has modified if(options.originalCharCount == current_char_count) return; //check minimum and maximum number of characters are typed if(current_char_count >= options.minCharLimit){ $(options.context).trigger('keyIn', [$(this).val()]); } else{ if(options.autocompleteFocused){ options.currentSelection = null; $(options.context).trigger('lostFocus'); } } } }); $(this).focus(function(){ //if the field is in a form capture the return key event $(this).closest("form").bind("keydown.block_for_smart_autocomplete", function(ev){ var type_ahead_field = $(options.context).prev('.smart_autocomplete_type_ahead_field'); if(ev.keyCode == '13'){ if(options.resultsContainer && $(options.resultsContainer).is(':visible')){ var current_selection = options.currentSelection; var result_suggestions = $(options.resultsContainer).children(); $(options.context).trigger('itemSelect', [ result_suggestions[current_selection] ] ); return false; } else if(options.typeAhead && type_ahead_field.is(':visible') ){ $(options.context).trigger('itemSelect', [ type_ahead_field ] ); return false; } } }); if(options.forceSelect){ $(this).select(); } }); //check for loosing focus on smart complete field and results container //$(this).blur(function(ev){ $(document).bind("focusin click", function(ev){ if(options.autocompleteFocused){ var elemIsParent = $.contains(options.resultsContainer[0], ev.target); if(ev.target == options.resultsContainer[0] || ev.target == options.context || elemIsParent) return $(options.context).closest("form").unbind("keydown.block_for_smart_autocomplete"); $(options.context).trigger('lostFocus'); } }); //bind events to results container $(options.resultsContainer).delegate(options.resultElement, 'mouseenter.smart_autocomplete', function(){ var current_selection = options.currentSelection || 0; var result_suggestions = $(options.resultsContainer).children(); options['currentSelection'] = $(this).prevAll().length; $(options.context).trigger('itemFocus', [this] ); }); $(options.resultsContainer).delegate(options.resultElement, 'mouseleave.smart_autocomplete', function(){ $(options.context).trigger('itemUnfocus', [this] ); }); $(options.resultsContainer).delegate(options.resultElement, 'click.smart_autocomplete', function(){ $(options.context).trigger('itemSelect', [this]); return false }); //bind plugin specific events $(this).bind({ keyIn: function(ev, query){ ev.smartAutocompleteData = {'query': query }; }, resultsReady: function(ev, results){ ev.smartAutocompleteData = {'results': results }; }, showResults: function(ev, results){ ev.smartAutocompleteData = {'results': results } }, noResults: function(){}, lostFocus: function(){}, itemSelect: function(ev, item){ ev.smartAutocompleteData = {'item': item }; }, itemFocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; }, itemUnfocus: function(ev, item){ ev.smartAutocompleteData = {'item': item }; } }); }); } })(jQuery); define("thirdparty/smart-auto-complete/jquery.smart_autocomplete", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ /** * Utilities for working with Deferred, Promise, and other asynchronous processes. */ define('utils/Async',['require','exports','module'],function (require, exports, module) { // Further ideas for Async utilities... // - Utilities for blocking UI until a Promise completes? // - A "SuperDeferred" could feature some very useful enhancements: // - API for cancellation (non guaranteed, best attempt) // - Easier way to add a timeout clause (withTimeout() wrapper below is more verbose) // - Encapsulate the task kickoff code so you can start it later, e.g. superDeferred.start() // - Deferred/Promise are unable to do anything akin to a 'finally' block. It'd be nice if we // could harvest exceptions across all steps of an async process and pipe them to a handler, // so that we don't leave UI-blocking overlays up forever, etc. But this is hard: we'd have // wrap every async callback (including low-level native ones that don't use [Super]Deferred) // to catch exceptions, and then understand which Deferred(s) the code *would* have resolved/ // rejected had it run to completion. /** * Executes a series of tasks in parallel, returning a "master" Promise that is resolved once * all the tasks have resolved. If one or more tasks fail, behavior depends on the failFast * flag: * - If true, the master Promise is rejected as soon as the first task fails. The remaining * tasks continue to completion in the background. * - If false, the master Promise is rejected after all tasks have completed. * * If nothing fails: (M = master promise; 1-4 = tasks; d = done; F = fail) * M ------------d * 1 >---d . * 2 >------d . * 3 >---------d . * 4 >------------d * * With failFast = false: * M ------------F * 1 >---d . . * 2 >------d . . * 3 >---------F . * 4 >------------d * * With failFast = true: -- equivalent to $.when() * M ---------F * 1 >---d . * 2 >------d . * 3 >---------F * 4 >------------d (#4 continues even though master Promise has failed) * (Note: if tasks finish synchronously, the behavior is more like failFast=false because you * won't get a chance to respond to the master Promise until after all items have been processed) * * To perform task-specific work after an individual task completes, attach handlers to each * Promise before beginProcessItem() returns it. * * Note: don't use this if individual tasks (or their done/fail handlers) could ever show a user- * visible dialog: because they run in parallel, you could show multiple dialogs atop each other. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @param {!boolean} failFast * @return {$.Promise} */ function doInParallel(items, beginProcessItem, failFast) { var promises = []; var masterDeferred = new $.Deferred(); if (items.length === 0) { masterDeferred.resolve(); } else { var numCompleted = 0; var hasFailed = false; items.forEach(function (item, i) { var itemPromise = beginProcessItem(item, i); promises.push(itemPromise); itemPromise.fail(function () { if (failFast) { masterDeferred.reject(); } else { hasFailed = true; } }); itemPromise.always(function () { numCompleted++; if (numCompleted === items.length) { if (hasFailed) { masterDeferred.reject(); } else { masterDeferred.resolve(); } } }); }); } return masterDeferred.promise(); } /** * Executes a series of tasks in serial (task N does not begin until task N-1 has completed). * Returns a "master" Promise that is resolved once all the tasks have resolved. If one or more * tasks fail, behavior depends on the failAndStopFast flag: * - If true, the master Promise is rejected as soon as the first task fails. The remaining * tasks are never started (the serial sequence is stopped). * - If false, the master Promise is rejected after all tasks have completed. * * If nothing fails: * M ------------d * 1 >---d . * 2 >--d . * 3 >--d . * 4 >--d * * With failAndStopFast = false: * M ------------F * 1 >---d . . * 2 >--d . . * 3 >--F . * 4 >--d * * With failAndStopFast = true: * M ---------F * 1 >---d . * 2 >--d . * 3 >--F * 4 (#4 never runs) * * To perform task-specific work after an individual task completes, attach handlers to each * Promise before beginProcessItem() returns it. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @param {!boolean} failAndStopFast * @return {$.Promise} */ function doSequentially(items, beginProcessItem, failAndStopFast) { var masterDeferred = new $.Deferred(), hasFailed = false; function doItem(i) { if (i >= items.length) { if (hasFailed) { masterDeferred.reject(); } else { masterDeferred.resolve(); } return; } var itemPromise = beginProcessItem(items[i], i); itemPromise.done(function () { doItem(i + 1); }); itemPromise.fail(function () { if (failAndStopFast) { masterDeferred.reject(); // note: we do NOT process any further items in this case } else { hasFailed = true; doItem(i + 1); } }); } doItem(0); return masterDeferred.promise(); } /** * Executes a series of tasks sequentially in time-slices less than maxBlockingTime. * Processing yields by idleTime between time-slices. * * @param {!Array.<*>} items * @param {!function(*, number):Promise} fnProcessItem * @param {!number} maxBlockingTime * @param {!number} idleTime * @return {$.Promise} */ function doSequentiallyInBackground(items, fnProcessItem, maxBlockingTime, idleTime) { maxBlockingTime = maxBlockingTime || 15; idleTime = idleTime || 30; var sliceStartTime = (new Date()).getTime(); return doSequentially(items, function (item, i) { var result = new $.Deferred(); // process the next item fnProcessItem(item, i); // if we've exhausted our maxBlockingTime if ((new Date()).getTime() - sliceStartTime >= maxBlockingTime) { //yield window.setTimeout(function () { sliceStartTime = (new Date()).getTime(); result.resolve(); }, idleTime); } else { //continue processing result.resolve(); } return result; }, false); } /** * Executes a series of tasks in parallel, saving up error info from any that fail along the way. * Returns a Promise that is only resolved/rejected once all tasks are complete. This is * essentially a wrapper around doInParallel(..., false). * * If one or more tasks failed, the entire "master" promise is rejected at the end - with one * argument: an array objects, one per failed task. Each error object contains: * - item -- the entry in items whose task failed * - error -- the first argument passed to the fail() handler when the task failed * * @param {!Array.<*>} items * @param {!function(*, number):Promise} beginProcessItem * @return {$.Promise} */ function doInParallel_aggregateErrors(items, beginProcessItem) { var errors = []; var masterDeferred = new $.Deferred(); var parallelResult = doInParallel( items, function (item, i) { var itemResult = beginProcessItem(item, i); itemResult.fail(function (error) { errors.push({ item: item, error: error }); }); return itemResult; }, false ); parallelResult .done(function () { masterDeferred.resolve(); }) .fail(function () { masterDeferred.reject(errors); }); return masterDeferred.promise(); } /** Value passed to fail() handlers that have been triggered due to withTimeout()'s timeout */ var ERROR_TIMEOUT = {}; /** * Adds timeout-driven failure to a Promise: returns a new Promise that is resolved/rejected when * the given original Promise is resolved/rejected, OR is rejected after the given delay - whichever * happens first. * * If the original Promise is resolved/rejected first, done()/fail() handlers receive arguments * piped from the original Promise. If the timeout occurs first instead, fail() is called with the * token Async.ERROR_TIMEOUT. * * @param {$.Promise} promise * @param {number} timeout * @return {$.Promise} */ function withTimeout(promise, timeout) { var wrapper = new $.Deferred(); var timer = window.setTimeout(function () { wrapper.reject(ERROR_TIMEOUT); }, timeout); promise.always(function () { window.clearTimeout(timer); }); // If the wrapper was already rejected due to timeout, the Promise's calls to resolve/reject // won't do anything promise.pipe(wrapper.resolve, wrapper.reject); return wrapper.promise(); } // Define public API exports.doInParallel = doInParallel; exports.doSequentially = doSequentially; exports.doSequentiallyInBackground = doSequentiallyInBackground; exports.doInParallel_aggregateErrors = doInParallel_aggregateErrors; exports.withTimeout = withTimeout; exports.ERROR_TIMEOUT = ERROR_TIMEOUT; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50*/ /*global $, define, brackets, FileError, InvalidateStateError */ define('file/NativeFileSystem',['require','exports','module','utils/Async'],function (require, exports, module) { var Async = require("utils/Async"); /* * Generally NativeFileSystem mimics the File API working draft * http://www.w3.org/TR/file-system-api/. The w3 entry point * requestFileSystem is replaced with our own requestNativeFileSystem. * * The current implementation is incomplete and noteably does not * support the Blob data type and synchronous APIs. DirectoryEntry * and FileEntry read/write capabilities are mostly implemented, but * delete is not. File writing is limited to UTF-8 text. */ var NativeFileSystem = { /** * Amount of time we wait for async calls to return (in milliseconds) * Not all async calls are wrapped with something that times out and * calls the error callback. Timeouts are not specified in the W3C spec. * @const * @type {number} */ ASYNC_TIMEOUT: 2000, /** showOpenDialog * * @param {bool} allowMultipleSelection * @param {bool} chooseDirectories * @param {string} title * @param {string} initialPath * @param {Array.<string>} fileTypes * @param {function(...)} successCallback * @param {function(...)} errorCallback * @constructor */ showOpenDialog: function (allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, successCallback, errorCallback) { if (!successCallback) { return; } var files = brackets.fs.showOpenDialog( allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, function (err, data) { if (!err) { successCallback(data); } else if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } } ); }, /** requestNativeFileSystem * * @param {string} path * @param {function(...)} successCallback * @param {function(...)} errorCallback */ requestNativeFileSystem: function (path, successCallback, errorCallback) { brackets.fs.stat(path, function (err, data) { if (!err) { // FIXME (issue #247): return a NativeFileSystem object var root = new NativeFileSystem.DirectoryEntry(path); successCallback(root); } else if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }, _nativeToFileError: function (nativeErr) { var error; switch (nativeErr) { // We map ERR_UNKNOWN and ERR_INVALID_PARAMS to SECURITY_ERR, // since there aren't specific mappings for these. case brackets.fs.ERR_UNKNOWN: case brackets.fs.ERR_INVALID_PARAMS: error = FileError.SECURITY_ERR; break; case brackets.fs.ERR_NOT_FOUND: error = FileError.NOT_FOUND_ERR; break; case brackets.fs.ERR_CANT_READ: error = FileError.NOT_READABLE_ERR; break; // It might seem like you should use FileError.ENCODING_ERR for this, // but according to the spec that's for malformed URLs. case brackets.fs.ERR_UNSUPPORTED_ENCODING: error = FileError.SECURITY_ERR; break; case brackets.fs.ERR_CANT_WRITE: error = FileError.NO_MODIFICATION_ALLOWED_ERR; break; case brackets.fs.ERR_OUT_OF_SPACE: error = FileError.QUOTA_EXCEEDED_ERR; break; case brackets.fs.PATH_EXISTS_ERR: error = FileError.PATH_EXISTS_ERR; break; default: // The HTML file spec says SECURITY_ERR is a catch-all to be used in situations // not covered by other error codes. error = FileError.SECURITY_ERR; } return new NativeFileSystem.FileError(error); } }; /** class: Encodings * * Static class that contains constants for file * encoding types. */ NativeFileSystem.Encodings = {}; NativeFileSystem.Encodings.UTF8 = "UTF-8"; NativeFileSystem.Encodings.UTF16 = "UTF-16"; /** class: _FSEncodings * * Internal static class that contains constants for file * encoding types to be used by internal file system * implimentation. */ NativeFileSystem._FSEncodings = {}; NativeFileSystem._FSEncodings.UTF8 = "utf8"; NativeFileSystem._FSEncodings.UTF16 = "utf16"; /** * Converts an IANA encoding name to internal encoding name. * http://www.iana.org/assignments/character-sets * * @param {String} encoding The IANA encoding string. */ NativeFileSystem.Encodings._IANAToFS = function (encoding) { //IANA names are case-insensitive encoding = encoding.toUpperCase(); switch (encoding) { case (NativeFileSystem.Encodings.UTF8): return NativeFileSystem._FSEncodings.UTF8; case (NativeFileSystem.Encodings.UTF16): return NativeFileSystem._FSEncodings.UTF16; default: return undefined; } }; var Encodings = NativeFileSystem.Encodings; var _FSEncodings = NativeFileSystem._FSEncodings; /** class: Entry * * @param {string} name * @param {string} isFile * @constructor */ NativeFileSystem.Entry = function (fullPath, isDirectory) { this.isDirectory = isDirectory; this.isFile = !isDirectory; if (fullPath) { // add trailing "/" to directory paths if (isDirectory && (fullPath.charAt(fullPath.length - 1) !== "/")) { fullPath = fullPath.concat("/"); } } this.fullPath = fullPath; this.name = null; // default if extraction fails if (fullPath) { var pathParts = fullPath.split("/"); // Extract name from the end of the fullPath (account for trailing slash(es)) while (!this.name && pathParts.length) { this.name = pathParts.pop(); } } // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-filesystem this.filesystem = null; }; NativeFileSystem.Entry.prototype.moveTo = function (parent, newName, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-moveTo }; NativeFileSystem.Entry.prototype.copyTo = function (parent, newName, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-copyTo }; NativeFileSystem.Entry.prototype.toURL = function (mimeType) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-toURL }; NativeFileSystem.Entry.prototype.remove = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-remove }; NativeFileSystem.Entry.prototype.getParent = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-remove }; NativeFileSystem.Entry.prototype.getMetadata = function (successCallBack, errorCallback) { brackets.fs.stat(this.fullPath, function (err, stat) { if (err === brackets.fs.NO_ERROR) { var metadata = new NativeFileSystem.Metadata(stat.mtime); successCallBack(metadata); } else { errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }; /** * Stores information about a FileEntry */ NativeFileSystem.Metadata = function (modificationTime) { // modificationTime is read only this.modificationTime = modificationTime; }; /** class: FileEntry * This interface represents a file on a file system. * * @param {string} name * @constructor * @extends {Entry} */ NativeFileSystem.FileEntry = function (name) { NativeFileSystem.Entry.call(this, name, false); }; NativeFileSystem.FileEntry.prototype = new NativeFileSystem.Entry(); NativeFileSystem.FileEntry.prototype.toString = function () { return "[FileEntry " + this.fullPath + "]"; }; /** * Creates a new FileWriter associated with the file that this FileEntry represents. * * @param {function (FileWriter)} successCallback * @param {function (FileError)} errorCallback */ NativeFileSystem.FileEntry.prototype.createWriter = function (successCallback, errorCallback) { var fileEntry = this; // [NoInterfaceObject] // interface FileWriter : FileSaver var FileWriter = function (data) { NativeFileSystem.FileSaver.call(this, data); // FileWriter private memeber vars this._length = 0; this._position = 0; }; FileWriter.prototype.length = function () { return this._length; }; FileWriter.prototype.position = function () { return this._position; }; // TODO (issue #241): handle Blob data instead of string FileWriter.prototype.write = function (data) { if (data === null || data === undefined) { throw new Error(); } if (this.readyState === NativeFileSystem.FileSaver.WRITING) { throw new NativeFileSystem.FileException(NativeFileSystem.FileException.INVALID_STATE_ERR); } this._readyState = NativeFileSystem.FileSaver.WRITING; if (this.onwritestart) { // TODO (issue #241): progressevent this.onwritestart(); } var self = this; brackets.fs.writeFile(fileEntry.fullPath, data, _FSEncodings.UTF8, function (err) { if ((err !== brackets.fs.NO_ERROR) && self.onerror) { var fileError = NativeFileSystem._nativeToFileError(err); // TODO (issue #241): set readonly FileSaver.error attribute // self._error = fileError; self.onerror(fileError); // TODO (issue #241): partial write, update length and position } // else { // TODO (issue #241): After changing data argument to Blob, use // Blob.size to update position and length upon successful // completion of a write. // self.position = ; // self.length = ; // } // DONE is set regardless of error self._readyState = NativeFileSystem.FileSaver.DONE; if (self.onwrite) { // TODO (issue #241): progressevent self.onwrite(); } if (self.onwriteend) { // TODO (issue #241): progressevent self.onwriteend(); } }); }; FileWriter.prototype.seek = function (offset) { }; FileWriter.prototype.truncate = function (size) { }; var fileWriter = new FileWriter(); // initialize file length var result = new $.Deferred(); brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { // Ignore "file not found" errors. It's okay if the file doesn't exist yet. if (err !== brackets.fs.ERR_NOT_FOUND) { fileWriter._err = err; } if (contents) { fileWriter._length = contents.length; } result.resolve(); }); result.done(function () { if (fileWriter._err && (errorCallback !== undefined)) { errorCallback(NativeFileSystem._nativeToFileError(fileWriter._err)); } else if (successCallback !== undefined) { successCallback(fileWriter); } }); }; /** * Obtains the File objecte for a FileEntry object * * @param {function(...)} successCallback * @param {function(...)} errorCallback */ NativeFileSystem.FileEntry.prototype.file = function (successCallback, errorCallback) { var newFile = new NativeFileSystem.File(this); successCallback(newFile); // TODO (issue #241): errorCallback }; /** * This interface extends the FileException interface described in to add * several new error codes. Any errors that need to be reported synchronously, * including all that occur during use of the synchronous filesystem methods, * are reported using the FileException exception. * * @param {number} code The code attribute, on getting, must return one of the * constants of the FileException exception, which must be the most appropriate * code from the table below. */ NativeFileSystem.FileException = function (code) { this.code = code || 0; }; // FileException constants Object.defineProperties( NativeFileSystem.FileException, { NOT_FOUND_ERR: { value: 1, writable: false }, SECURITY_ERR: { value: 2, writable: false }, ABORT_ERR: { value: 3, writable: false }, NOT_READABLE_ERR: { value: 4, writable: false }, ENCODING_ERR: { value: 5, writable: false }, NO_MODIFICATION_ALLOWED_ERR: { value: 6, writable: false }, INVALID_STATE_ERR: { value: 7, writable: false }, SYNTAX_ERR: { value: 8, writable: false }, QUOTA_EXCEEDED_ERR: { value: 10, writable: false } } ); /** * This interface provides methods to monitor the asynchronous writing of blobs * to disk using progress events and event handler attributes. * * This interface is specified to be used within the context of the global * object (Window) and within Web Workers. * * @param {Blob} data * @constructor */ NativeFileSystem.FileSaver = function (data) { // FileSaver private member vars this._data = data; this._readyState = NativeFileSystem.FileSaver.INIT; this._error = null; }; // FileSaver constants Object.defineProperties( NativeFileSystem.FileSaver, { INIT: { value: 1, writable: false }, WRITING: { value: 2, writable: false }, DONE: { value: 3, writable: false } } ); // FileSaver methods /** * */ NativeFileSystem.FileSaver.prototype.readyState = function () { return this._readyState; }; // TODO (issue #241): http://dev.w3.org/2009/dap/file-system/file-writer.html#widl-FileSaver-abort-void NativeFileSystem.FileSaver.prototype.abort = function () { // If readyState is DONE or INIT, terminate this overall series of steps without doing anything else.. if (this._readyState === NativeFileSystem.FileSaver.INIT || this._readyState === NativeFileSystem.FileSaver.DONE) { return; } // TODO (issue #241): Terminate any steps having to do with writing a file. // Set the error attribute to a FileError object with the code ABORT_ERR. this._error = new NativeFileSystem.FileError(FileError.ABORT_ERR); // Set readyState to DONE. this._readyState = NativeFileSystem.FileSaver.DONE; /* TODO (issue #241): Dispatch a progress event called abort Dispatch a progress event called writeend Stop dispatching any further progress events. Terminate this overall set of steps. */ }; /** * This interface represents a directory on a file system. * * @constructor * @param {string} name * @extends {Entry} */ NativeFileSystem.DirectoryEntry = function (name) { NativeFileSystem.Entry.call(this, name, true); // TODO (issue #241): void removeRecursively (VoidCallback successCallback, optional ErrorCallback errorCallback); }; NativeFileSystem.DirectoryEntry.prototype = new NativeFileSystem.Entry(); NativeFileSystem.DirectoryEntry.prototype.toString = function () { return "[DirectoryEntry " + this.fullPath + "]"; }; NativeFileSystem.DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-getDirectory }; NativeFileSystem.DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) { // TODO (issue #241) // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-removeRecursively }; NativeFileSystem.DirectoryEntry.prototype.createReader = function () { var dirReader = new NativeFileSystem.DirectoryReader(); dirReader._directory = this; return dirReader; }; /** * Creates or looks up a file. * * @param {string} path Either an absolute path or a relative path from this * DirectoryEntry to the file to be looked up or created. It is an error * to attempt to create a file whose immediate parent does not yet * exist. * @param {Object.<string, boolean>} options * @param {function (number)} successCallback * @param {function (number)} errorCallback */ NativeFileSystem.DirectoryEntry.prototype.getFile = function (path, options, successCallback, errorCallback) { var fileFullPath = path; function isRelativePath(path) { // If the path contains a colons it must be a full path on Windows (colons are // not valid path characters on mac or in URIs) if (path.indexOf(":") !== -1) { return false; } // For everyone else, absolute paths start with a "/" return path[0] !== "/"; } // resolve relative paths relative to the DirectoryEntry if (isRelativePath(path)) { fileFullPath = this.fullPath + path; } var createFileEntry = function () { if (successCallback) { successCallback(new NativeFileSystem.FileEntry(fileFullPath)); } }; var createFileError = function (err) { if (errorCallback) { errorCallback(NativeFileSystem._nativeToFileError(err)); } }; // Use stat() to check if file exists brackets.fs.stat(fileFullPath, function (err, stats) { if ((err === brackets.fs.NO_ERROR)) { // NO_ERROR implies the path already exists // throw error if the file the path is a directory if (stats.isDirectory()) { if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.TYPE_MISMATCH_ERR)); } return; } // throw error if the file exists but create is exclusive if (options.create && options.exclusive) { if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.PATH_EXISTS_ERR)); } return; } // Create a file entry for the existing file. If create == true, // a file entry is created without error. createFileEntry(); } else if (err === brackets.fs.ERR_NOT_FOUND) { // ERR_NOT_FOUND implies we write a new, empty file // create the file if (options.create) { brackets.fs.writeFile(fileFullPath, "", _FSEncodings.UTF8, function (err) { if (err) { createFileError(err); } else { createFileEntry(); } }); return; } // throw error if file not found and the create == false if (errorCallback) { errorCallback(new NativeFileSystem.FileError(FileError.NOT_FOUND_ERR)); } } else { // all other brackets.fs.stat() errors createFileError(err); } }); }; /** class: DirectoryReader */ NativeFileSystem.DirectoryReader = function () { }; /** readEntries * * @param {function(...)} successCallback * @param {function(...)} errorCallback * @returns {Array.<Entry>} */ NativeFileSystem.DirectoryReader.prototype.readEntries = function (successCallback, errorCallback) { var rootPath = this._directory.fullPath; brackets.fs.readdir(rootPath, function (err, filelist) { if (!err) { var entries = []; var lastError = null; // stat() to determine type of each entry, then populare entries array with objects var masterPromise = Async.doInParallel(filelist, function (filename, index) { var deferred = new $.Deferred(); var itemFullPath = rootPath + filelist[index]; brackets.fs.stat(itemFullPath, function (statErr, statData) { if (!statErr) { if (statData.isDirectory()) { entries[index] = new NativeFileSystem.DirectoryEntry(itemFullPath); } else if (statData.isFile()) { entries[index] = new NativeFileSystem.FileEntry(itemFullPath); } else { entries[index] = null; // neither a file nor a dir, so don't include it } deferred.resolve(); } else { lastError = NativeFileSystem._nativeToFileError(statErr); deferred.reject(lastError); } }); return deferred.promise(); }, true); // We want the error callback to get called after some timeout (in case some deferreds don't return). // So, we need to wrap masterPromise in another deferred that has this timeout functionality var timeoutWrapper = Async.withTimeout(masterPromise, NativeFileSystem.ASYNC_TIMEOUT); // Add the callbacks to this top-level Promise, which wraps all the individual deferred objects timeoutWrapper.then( function () { // success // The entries array may have null values if stat returned things that were // neither a file nor a dir. So, we need to clean those out. var cleanedEntries = [], i; for (i = 0; i < entries.length; i++) { if (entries[i]) { cleanedEntries.push(entries[i]); } } successCallback(cleanedEntries); }, function (err) { // error if (err === Async.ERROR_TIMEOUT) { // SECURITY_ERR is the HTML5 File catch-all error, and there isn't anything // more fitting for a timeout. err = new NativeFileSystem.FileError(FileError.SECURITY_ERR); } else { err = lastError; } if (errorCallback) { errorCallback(err); } } ); } else { // There was an error reading the initial directory. errorCallback(NativeFileSystem._nativeToFileError(err)); } }); }; /** class: FileReader * * @extends {EventTarget} */ NativeFileSystem.FileReader = function () { // TODO (issue #241): this classes should extend EventTarget // states this.EMPTY = 0; this.LOADING = 1; this.DONE = 2; // readyState is read only this.readyState = this.EMPTY; // File or Blob data // TODO (issue #241): readonly attribute any result; // TODO (issue #241): readonly attribute DOMError error; // event handler attributes this.onloadstart = null; this.onprogress = null; this.onload = null; this.onabort = null; this.onerror = null; this.onloadend = null; }; // TODO (issue #241): extend EventTarget (draft status, not implememnted in webkit) // NativeFileSystem.FileReader.prototype = new NativeFileSystem.EventTarget() NativeFileSystem.FileReader.prototype.readAsArrayBuffer = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsArrayBuffer }; NativeFileSystem.FileReader.prototype.readAsBinaryString = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsBinaryStringAsync }; NativeFileSystem.FileReader.prototype.readAsDataURL = function (blob) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsDataURL }; NativeFileSystem.FileReader.prototype.abort = function () { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-abort }; /** readAsText * * @param {Blob} blob * @param {string} encoding (IANA Encoding Name) */ NativeFileSystem.FileReader.prototype.readAsText = function (blob, encoding) { var self = this; if (!encoding) { encoding = Encodings.UTF8; } var internalEncoding = Encodings._IANAToFS(encoding); if (this.readyState === this.LOADING) { throw new InvalidateStateError(); } this.readyState = this.LOADING; if (this.onloadstart) { this.onloadstart(); // TODO (issue #241): progressevent } brackets.fs.readFile(blob._fullPath, internalEncoding, function (err, data) { // TODO (issue #241): the event objects passed to these event handlers is fake and incomplete right now var fakeEvent = { loaded: 0, total: 0 }; // The target for this event is the FileReader and the data/err result is stored in the FileReader fakeEvent.target = self; self.result = data; self.error = NativeFileSystem._nativeToFileError(err); if (err) { self.readyState = self.DONE; if (self.onerror) { self.onerror(fakeEvent); } } else { self.readyState = self.DONE; // TODO (issue #241): this should be the file/blob size, but we don't have code to get that yet, so for know assume a file size of 1 // and since we read the file in one go, assume 100% after the first read fakeEvent.loaded = 1; fakeEvent.total = 1; if (self.onprogress) { self.onprogress(fakeEvent); } // TODO (issue #241): onabort not currently supported since our native implementation doesn't support it // if (self.onabort) // self.onabort(fakeEvent); if (self.onload) { self.onload(fakeEvent); } if (self.onloadend) { self.onloadend(); } } }); }; /** class: Blob * * @constructor * param {Entry} entry */ NativeFileSystem.Blob = function (fullPath) { this._fullPath = fullPath; // TODO (issue #241): implement, readonly this.size = 0; // TODO (issue #241): implement, readonly this.type = null; }; NativeFileSystem.Blob.prototype.slice = function (start, end, contentType) { // TODO (issue #241): implement // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-slice }; /** class: File * * @constructor * param {Entry} entry * @extends {Blob} */ NativeFileSystem.File = function (entry) { NativeFileSystem.Blob.call(this, entry.fullPath); // TODO (issue #241): implement, readonly this.name = ""; // TODO (issue #241): implement, readonly this.lastModifiedDate = null; }; /** class: FileError * * Implementation of HTML file API error code return class. Note that we don't * actually define the error codes here--we rely on the browser's built-in FileError * class's constants. In other words, external clients of this API should always * use FileError.<constant-name>, not NativeFileSystem.FileError.<constant-name>. * * @constructor * @param {number} code The error code to return with this FileError. Must be * one of the codes defined in the FileError class. */ NativeFileSystem.FileError = function (code) { this.code = code || 0; }; // Define public API exports.NativeFileSystem = NativeFileSystem; }); /* * jsTree 1.0-rc3 * http://jstree.com/ * * Copyright (c) 2010 Ivan Bozhanov (vakata.com) * * Licensed same as jquery - under the terms of either the MIT License or the GPL Version 2 License * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * $Date: 2011-02-09 01:17:14 +0200 (ср, 09 февр 2011) $ * $Revision: 236 $ */ /*jslint browser: true, onevar: true, undef: true, bitwise: true, strict: true */ /*global window : false, clearInterval: false, clearTimeout: false, document: false, setInterval: false, setTimeout: false, jQuery: false, navigator: false, XSLTProcessor: false, DOMParser: false, XMLSerializer: false*/ // top wrapper to prevent multiple inclusion (is this OK?) (function () { if(jQuery && jQuery.jstree) { return; } var is_ie6 = false, is_ie7 = false, is_ff2 = false; /* * jsTree core */ (function ($) { // Common functions not related to jsTree // decided to move them to a `vakata` "namespace" $.vakata = {}; // CSS related functions $.vakata.css = { get_css : function(rule_name, delete_flag, sheet) { rule_name = rule_name.toLowerCase(); var css_rules = sheet.cssRules || sheet.rules, j = 0; do { if(css_rules.length && j > css_rules.length + 5) { return false; } if(css_rules[j].selectorText && css_rules[j].selectorText.toLowerCase() == rule_name) { if(delete_flag === true) { if(sheet.removeRule) { sheet.removeRule(j); } if(sheet.deleteRule) { sheet.deleteRule(j); } return true; } else { return css_rules[j]; } } } while (css_rules[++j]); return false; }, add_css : function(rule_name, sheet) { if($.jstree.css.get_css(rule_name, false, sheet)) { return false; } if(sheet.insertRule) { sheet.insertRule(rule_name + ' { }', 0); } else { sheet.addRule(rule_name, null, 0); } return $.vakata.css.get_css(rule_name); }, remove_css : function(rule_name, sheet) { return $.vakata.css.get_css(rule_name, true, sheet); }, add_sheet : function(opts) { var tmp = false, is_new = true; if(opts.str) { if(opts.title) { tmp = $("style[id='" + opts.title + "-stylesheet']")[0]; } if(tmp) { is_new = false; } else { tmp = document.createElement("style"); tmp.setAttribute('type',"text/css"); if(opts.title) { tmp.setAttribute("id", opts.title + "-stylesheet"); } } if(tmp.styleSheet) { if(is_new) { document.getElementsByTagName("head")[0].appendChild(tmp); tmp.styleSheet.cssText = opts.str; } else { tmp.styleSheet.cssText = tmp.styleSheet.cssText + " " + opts.str; } } else { tmp.appendChild(document.createTextNode(opts.str)); document.getElementsByTagName("head")[0].appendChild(tmp); } return tmp.sheet || tmp.styleSheet; } if(opts.url) { if(document.createStyleSheet) { try { tmp = document.createStyleSheet(opts.url); } catch (e) { } } else { tmp = document.createElement('link'); tmp.rel = 'stylesheet'; tmp.type = 'text/css'; tmp.media = "all"; tmp.href = opts.url; document.getElementsByTagName("head")[0].appendChild(tmp); return tmp.styleSheet; } } } }; // private variables var instances = [], // instance array (used by $.jstree.reference/create/focused) focused_instance = -1, // the index in the instance array of the currently focused instance plugins = {}, // list of included plugins prepared_move = {}; // for the move_node function // jQuery plugin wrapper (thanks to jquery UI widget function) $.fn.jstree = function (settings) { var isMethodCall = (typeof settings == 'string'), // is this a method call like $().jstree("open_node") args = Array.prototype.slice.call(arguments, 1), returnValue = this; // if a method call execute the method on all selected instances if(isMethodCall) { if(settings.substring(0, 1) == '_') { return returnValue; } this.each(function() { var instance = instances[$.data(this, "jstree_instance_id")], methodValue = (instance && $.isFunction(instance[settings])) ? instance[settings].apply(instance, args) : instance; if(typeof methodValue !== "undefined" && (settings.indexOf("is_") === 0 || (methodValue !== true && methodValue !== false))) { returnValue = methodValue; return false; } }); } else { this.each(function() { // extend settings and allow for multiple hashes and $.data var instance_id = $.data(this, "jstree_instance_id"), a = [], b = settings ? $.extend({}, true, settings) : {}, c = $(this), s = false, t = []; a = a.concat(args); if(c.data("jstree")) { a.push(c.data("jstree")); } b = a.length ? $.extend.apply(null, [true, b].concat(a)) : b; // if an instance already exists, destroy it first if(typeof instance_id !== "undefined" && instances[instance_id]) { instances[instance_id].destroy(); } // push a new empty object to the instances array instance_id = parseInt(instances.push({}),10) - 1; // store the jstree instance id to the container element $.data(this, "jstree_instance_id", instance_id); // clean up all plugins b.plugins = $.isArray(b.plugins) ? b.plugins : $.jstree.defaults.plugins.slice(); b.plugins.unshift("core"); // only unique plugins b.plugins = b.plugins.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); // extend defaults with passed data s = $.extend(true, {}, $.jstree.defaults, b); s.plugins = b.plugins; $.each(plugins, function (i, val) { if($.inArray(i, s.plugins) === -1) { s[i] = null; delete s[i]; } else { t.push(i); } }); s.plugins = t; // push the new object to the instances array (at the same time set the default classes to the container) and init instances[instance_id] = new $.jstree._instance(instance_id, $(this).addClass("jstree jstree-" + instance_id), s); // init all activated plugins for this instance $.each(instances[instance_id]._get_settings().plugins, function (i, val) { instances[instance_id].data[val] = {}; }); $.each(instances[instance_id]._get_settings().plugins, function (i, val) { if(plugins[val]) { plugins[val].__init.apply(instances[instance_id]); } }); // initialize the instance setTimeout(function() { if(instances[instance_id]) { instances[instance_id].init(); } }, 0); }); } // return the jquery selection (or if it was a method call that returned a value - the returned value) return returnValue; }; // object to store exposed functions and objects $.jstree = { defaults : { plugins : [] }, _focused : function () { return instances[focused_instance] || null; }, _reference : function (needle) { // get by instance id if(instances[needle]) { return instances[needle]; } // get by DOM (if still no luck - return null var o = $(needle); if(!o.length && typeof needle === "string") { o = $("#" + needle); } if(!o.length) { return null; } return instances[o.closest(".jstree").data("jstree_instance_id")] || null; }, _instance : function (index, container, settings) { // for plugins to store data in this.data = { core : {} }; this.get_settings = function () { return $.extend(true, {}, settings); }; this._get_settings = function () { return settings; }; this.get_index = function () { return index; }; this.get_container = function () { return container; }; this.get_container_ul = function () { return container.children("ul:eq(0)"); }; this._set_settings = function (s) { settings = $.extend(true, {}, settings, s); }; }, _fn : { }, plugin : function (pname, pdata) { pdata = $.extend({}, { __init : $.noop, __destroy : $.noop, _fn : {}, defaults : false }, pdata); plugins[pname] = pdata; $.jstree.defaults[pname] = pdata.defaults; $.each(pdata._fn, function (i, val) { val.plugin = pname; val.old = $.jstree._fn[i]; $.jstree._fn[i] = function () { var rslt, func = val, args = Array.prototype.slice.call(arguments), evnt = new $.Event("before.jstree"), rlbk = false; if(this.data.core.locked === true && i !== "unlock" && i !== "is_locked") { return; } // Check if function belongs to the included plugins of this instance do { if(func && func.plugin && $.inArray(func.plugin, this._get_settings().plugins) !== -1) { break; } func = func.old; } while(func); if(!func) { return; } // context and function to trigger events, then finally call the function if(i.indexOf("_") === 0) { rslt = func.apply(this, args); } else { rslt = this.get_container().triggerHandler(evnt, { "func" : i, "inst" : this, "args" : args, "plugin" : func.plugin }); if(rslt === false) { return; } if(typeof rslt !== "undefined") { args = rslt; } rslt = func.apply( $.extend({}, this, { __callback : function (data) { this.get_container().triggerHandler( i + '.jstree', { "inst" : this, "args" : args, "rslt" : data, "rlbk" : rlbk }); }, __rollback : function () { rlbk = this.get_rollback(); return rlbk; }, __call_old : function (replace_arguments) { return func.old.apply(this, (replace_arguments ? Array.prototype.slice.call(arguments, 1) : args ) ); } }), args); } // return the result return rslt; }; $.jstree._fn[i].old = val.old; $.jstree._fn[i].plugin = pname; }); }, rollback : function (rb) { if(rb) { if(!$.isArray(rb)) { rb = [ rb ]; } $.each(rb, function (i, val) { instances[val.i].set_rollback(val.h, val.d); }); } } }; // set the prototype for all instances $.jstree._fn = $.jstree._instance.prototype = {}; // load the css when DOM is ready $(function() { // code is copied from jQuery ($.browser is deprecated + there is a bug in IE) var u = navigator.userAgent.toLowerCase(), v = (u.match( /.+?(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1], css_string = '' + '.jstree ul, .jstree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } ' + '.jstree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } ' + '.jstree-rtl li { margin-left:0; margin-right:18px; } ' + '.jstree > ul > li { margin-left:0px; } ' + '.jstree-rtl > ul > li { margin-right:0px; } ' + '.jstree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } ' + '.jstree a { display:inline-block; line-height:16px; height:16px; color:black; white-space:nowrap; text-decoration:none; padding:1px 2px; margin:0; } ' + '.jstree a:focus { outline: none; } ' + '.jstree a > ins { height:16px; width:16px; } ' + '.jstree a > .jstree-icon { margin-right:3px; } ' + '.jstree-rtl a > .jstree-icon { margin-left:3px; margin-right:0; } ' + 'li.jstree-open > ul { display:block; } ' + 'li.jstree-closed > ul { display:none; } '; // Correct IE 6 (does not support the > CSS selector) if(/msie/.test(u) && parseInt(v, 10) == 6) { is_ie6 = true; // fix image flicker and lack of caching try { document.execCommand("BackgroundImageCache", false, true); } catch (err) { } css_string += '' + '.jstree li { height:18px; margin-left:0; margin-right:0; } ' + '.jstree li li { margin-left:18px; } ' + '.jstree-rtl li li { margin-left:0px; margin-right:18px; } ' + 'li.jstree-open ul { display:block; } ' + 'li.jstree-closed ul { display:none !important; } ' + '.jstree li a { display:inline; border-width:0 !important; padding:0px 2px !important; } ' + '.jstree li a ins { height:16px; width:16px; margin-right:3px; } ' + '.jstree-rtl li a ins { margin-right:0px; margin-left:3px; } '; } // Correct IE 7 (shifts anchor nodes onhover) if(/msie/.test(u) && parseInt(v, 10) == 7) { is_ie7 = true; css_string += '.jstree li a { border-width:0 !important; padding:0px 2px !important; } '; } // correct ff2 lack of display:inline-block if(!/compatible/.test(u) && /mozilla/.test(u) && parseFloat(v, 10) < 1.9) { is_ff2 = true; css_string += '' + '.jstree ins { display:-moz-inline-box; } ' + '.jstree li { line-height:12px; } ' + // WHY?? '.jstree a { display:-moz-inline-box; } ' + '.jstree .jstree-no-icons .jstree-checkbox { display:-moz-inline-stack !important; } '; /* this shouldn't be here as it is theme specific */ } // the default stylesheet $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); // core functions (open, close, create, update, delete) $.jstree.plugin("core", { __init : function () { this.data.core.locked = false; this.data.core.to_open = this.get_settings().core.initially_open; this.data.core.to_load = this.get_settings().core.initially_load; }, defaults : { html_titles : false, animation : 500, initially_open : [], initially_load : [], open_parents : true, notify_plugins : true, rtl : false, load_open : false, strings : { loading : "Loading ...", new_node : "New node", multiple_selection : "Multiple selection" } }, _fn : { init : function () { this.set_focus(); if(this._get_settings().core.rtl) { this.get_container().addClass("jstree-rtl").css("direction", "rtl"); } this.get_container().html("<ul><li class='jstree-last jstree-leaf'><ins> </ins><a class='jstree-loading' href='#'><ins class='jstree-icon'> </ins>" + this._get_string("loading") + "</a></li></ul>"); this.data.core.li_height = this.get_container_ul().find("li.jstree-closed, li.jstree-leaf").eq(0).height() || 18; this.get_container() .delegate("li > ins", "click.jstree", $.proxy(function (event) { var trgt = $(event.target); // if(trgt.is("ins") && event.pageY - trgt.offset().top < this.data.core.li_height) { this.toggle_node(trgt); } this.toggle_node(trgt); }, this)) .bind("mousedown.jstree", $.proxy(function () { this.set_focus(); // This used to be setTimeout(set_focus,0) - why? }, this)) .bind("dblclick.jstree", function (event) { var sel; if(document.selection && document.selection.empty) { document.selection.empty(); } else { if(window.getSelection) { sel = window.getSelection(); try { sel.removeAllRanges(); sel.collapse(); } catch (err) { } } } }); if(this._get_settings().core.notify_plugins) { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var o = this._get_node(data.rslt.obj), t = this; if(o === -1) { o = this.get_container_ul(); } if(!o.length) { return; } o.find("li").each(function () { var th = $(this); if(th.data("jstree")) { $.each(th.data("jstree"), function (plugin, values) { if(t.data[plugin] && $.isFunction(t["_" + plugin + "_notify"])) { t["_" + plugin + "_notify"].call(t, th, values); } }); } }); }, this)); } if(this._get_settings().core.load_open) { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var o = this._get_node(data.rslt.obj), t = this; if(o === -1) { o = this.get_container_ul(); } if(!o.length) { return; } o.find("li.jstree-open:not(:has(ul))").each(function () { t.load_node(this, $.noop, $.noop); }); }, this)); } this.__callback(); this.load_node(-1, function () { this.loaded(); this.reload_nodes(); }); }, destroy : function () { var i, n = this.get_index(), s = this._get_settings(), _this = this; $.each(s.plugins, function (i, val) { try { plugins[val].__destroy.apply(_this); } catch(err) { } }); this.__callback(); // set focus to another instance if this one is focused if(this.is_focused()) { for(i in instances) { if(instances.hasOwnProperty(i) && i != n) { instances[i].set_focus(); break; } } } // if no other instance found if(n === focused_instance) { focused_instance = -1; } // remove all traces of jstree in the DOM (only the ones set using jstree*) and cleans all events this.get_container() .unbind(".jstree") .undelegate(".jstree") .removeData("jstree_instance_id") .find("[class^='jstree']") .andSelf() .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); $(document) .unbind(".jstree-" + n) .undelegate(".jstree-" + n); // remove the actual data instances[n] = null; delete instances[n]; }, _core_notify : function (n, data) { if(data.opened) { this.open_node(n, false, true); } }, lock : function () { this.data.core.locked = true; this.get_container().children("ul").addClass("jstree-locked").css("opacity","0.7"); this.__callback({}); }, unlock : function () { this.data.core.locked = false; this.get_container().children("ul").removeClass("jstree-locked").css("opacity","1"); this.__callback({}); }, is_locked : function () { return this.data.core.locked; }, save_opened : function () { var _this = this; this.data.core.to_open = []; this.get_container_ul().find("li.jstree-open").each(function () { if(this.id) { _this.data.core.to_open.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } }); this.__callback(_this.data.core.to_open); }, save_loaded : function () { }, reload_nodes : function (is_callback) { var _this = this, done = true, current = [], remaining = []; if(!is_callback) { this.data.core.reopen = false; this.data.core.refreshing = true; this.data.core.to_open = $.map($.makeArray(this.data.core.to_open), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); this.data.core.to_load = $.map($.makeArray(this.data.core.to_load), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); if(this.data.core.to_open.length) { this.data.core.to_load = this.data.core.to_load.concat(this.data.core.to_open); } } if(this.data.core.to_load.length) { $.each(this.data.core.to_load, function (i, val) { if(val == "#") { return true; } if($(val).length) { current.push(val); } else { remaining.push(val); } }); if(current.length) { this.data.core.to_load = remaining; $.each(current, function (i, val) { if(!_this._is_loaded(val)) { _this.load_node(val, function () { _this.reload_nodes(true); }, function () { _this.reload_nodes(true); }); done = false; } }); } } if(this.data.core.to_open.length) { $.each(this.data.core.to_open, function (i, val) { _this.open_node(val, false, true); }); } if(done) { // TODO: find a more elegant approach to syncronizing returning requests if(this.data.core.reopen) { clearTimeout(this.data.core.reopen); } this.data.core.reopen = setTimeout(function () { _this.__callback({}, _this); }, 50); this.data.core.refreshing = false; this.reopen(); } }, reopen : function () { var _this = this; if(this.data.core.to_open.length) { $.each(this.data.core.to_open, function (i, val) { _this.open_node(val, false, true); }); } this.__callback({}); }, refresh : function (obj) { var _this = this; this.save_opened(); if(!obj) { obj = -1; } obj = this._get_node(obj); if(!obj) { obj = -1; } if(obj !== -1) { obj.children("UL").remove(); } else { this.get_container_ul().empty(); } this.load_node(obj, function () { _this.__callback({ "obj" : obj}); _this.reload_nodes(); }); }, // Dummy function to fire after the first load (so that there is a jstree.loaded event) loaded : function () { this.__callback(); }, // deal with focus set_focus : function () { if(this.is_focused()) { return; } var f = $.jstree._focused(); if(f) { f.unset_focus(); } this.get_container().addClass("jstree-focused"); focused_instance = this.get_index(); this.__callback(); }, is_focused : function () { return focused_instance == this.get_index(); }, unset_focus : function () { if(this.is_focused()) { this.get_container().removeClass("jstree-focused"); focused_instance = -1; } this.__callback(); }, // traverse _get_node : function (obj) { var $obj = $(obj, this.get_container()); if($obj.is(".jstree") || obj == -1) { return -1; } $obj = $obj.closest("li", this.get_container()); return $obj.length ? $obj : false; }, _get_next : function (obj, strict) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().find("> ul > li:first-child"); } if(!obj.length) { return false; } if(strict) { return (obj.nextAll("li").size() > 0) ? obj.nextAll("li:eq(0)") : false; } if(obj.hasClass("jstree-open")) { return obj.find("li:eq(0)"); } else if(obj.nextAll("li").size() > 0) { return obj.nextAll("li:eq(0)"); } else { return obj.parentsUntil(".jstree","li").next("li").eq(0); } }, _get_prev : function (obj, strict) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().find("> ul > li:last-child"); } if(!obj.length) { return false; } if(strict) { return (obj.prevAll("li").length > 0) ? obj.prevAll("li:eq(0)") : false; } if(obj.prev("li").length) { obj = obj.prev("li").eq(0); while(obj.hasClass("jstree-open")) { obj = obj.children("ul:eq(0)").children("li:last"); } return obj; } else { var o = obj.parentsUntil(".jstree","li:eq(0)"); return o.length ? o : false; } }, _get_parent : function (obj) { obj = this._get_node(obj); if(obj == -1 || !obj.length) { return false; } var o = obj.parentsUntil(".jstree", "li:eq(0)"); return o.length ? o : -1; }, _get_children : function (obj) { obj = this._get_node(obj); if(obj === -1) { return this.get_container().children("ul:eq(0)").children("li"); } if(!obj.length) { return false; } return obj.children("ul:eq(0)").children("li"); }, get_path : function (obj, id_mode) { var p = [], _this = this; obj = this._get_node(obj); if(obj === -1 || !obj || !obj.length) { return false; } obj.parentsUntil(".jstree", "li").each(function () { p.push( id_mode ? this.id : _this.get_text(this) ); }); p.reverse(); p.push( id_mode ? obj.attr("id") : this.get_text(obj) ); return p; }, // string functions _get_string : function (key) { return this._get_settings().core.strings[key] || key; }, is_open : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-open"); }, is_closed : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-closed"); }, is_leaf : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-leaf"); }, correct_state : function (obj) { obj = this._get_node(obj); if(!obj || obj === -1) { return false; } obj.removeClass("jstree-closed jstree-open").addClass("jstree-leaf").children("ul").remove(); this.__callback({ "obj" : obj }); }, // open/close open_node : function (obj, callback, skip_animation) { obj = this._get_node(obj); if(!obj.length) { return false; } if(!obj.hasClass("jstree-closed")) { if(callback) { callback.call(); } return false; } var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, t = this; if(!this._is_loaded(obj)) { obj.children("a").addClass("jstree-loading"); this.load_node(obj, function () { t.open_node(obj, callback, skip_animation); }, callback); } else { if(this._get_settings().core.open_parents) { obj.parentsUntil(".jstree",".jstree-closed").each(function () { t.open_node(this, false, true); }); } if(s) { obj.children("ul").css("display","none"); } obj.removeClass("jstree-closed").addClass("jstree-open").children("a").removeClass("jstree-loading"); if(s) { obj.children("ul").stop(true, true).slideDown(s, function () { this.style.display = ""; t.after_open(obj); }); } else { t.after_open(obj); } this.__callback({ "obj" : obj }); if(callback) { callback.call(); } } }, after_open : function (obj) { this.__callback({ "obj" : obj }); }, close_node : function (obj, skip_animation) { obj = this._get_node(obj); var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, t = this; if(!obj.length || !obj.hasClass("jstree-open")) { return false; } if(s) { obj.children("ul").attr("style","display:block !important"); } obj.removeClass("jstree-open").addClass("jstree-closed"); if(s) { obj.children("ul").stop(true, true).slideUp(s, function () { this.style.display = ""; t.after_close(obj); }); } else { t.after_close(obj); } this.__callback({ "obj" : obj }); }, after_close : function (obj) { this.__callback({ "obj" : obj }); }, toggle_node : function (obj) { obj = this._get_node(obj); if(obj.hasClass("jstree-closed")) { return this.open_node(obj); } if(obj.hasClass("jstree-open")) { return this.close_node(obj); } }, open_all : function (obj, do_animation, original_obj) { obj = obj ? this._get_node(obj) : -1; if(!obj || obj === -1) { obj = this.get_container_ul(); } if(original_obj) { obj = obj.find("li.jstree-closed"); } else { original_obj = obj; if(obj.is(".jstree-closed")) { obj = obj.find("li.jstree-closed").andSelf(); } else { obj = obj.find("li.jstree-closed"); } } var _this = this; obj.each(function () { var __this = this; if(!_this._is_loaded(this)) { _this.open_node(this, function() { _this.open_all(__this, do_animation, original_obj); }, !do_animation); } else { _this.open_node(this, false, !do_animation); } }); // so that callback is fired AFTER all nodes are open if(original_obj.find('li.jstree-closed').length === 0) { this.__callback({ "obj" : original_obj }); } }, close_all : function (obj, do_animation) { var _this = this; obj = obj ? this._get_node(obj) : this.get_container(); if(!obj || obj === -1) { obj = this.get_container_ul(); } obj.find("li.jstree-open").andSelf().each(function () { _this.close_node(this, !do_animation); }); this.__callback({ "obj" : obj }); }, clean_node : function (obj) { obj = obj && obj != -1 ? $(obj) : this.get_container_ul(); obj = obj.is("li") ? obj.find("li").andSelf() : obj.find("li"); obj.removeClass("jstree-last") .filter("li:last-child").addClass("jstree-last").end() .filter(":has(li)") .not(".jstree-open").removeClass("jstree-leaf").addClass("jstree-closed"); obj.not(".jstree-open, .jstree-closed").addClass("jstree-leaf").children("ul").remove(); this.__callback({ "obj" : obj }); }, // rollback get_rollback : function () { this.__callback(); return { i : this.get_index(), h : this.get_container().children("ul").clone(true), d : this.data }; }, set_rollback : function (html, data) { this.get_container().empty().append(html); this.data = data; this.__callback(); }, // Dummy functions to be overwritten by any datastore plugin included load_node : function (obj, s_call, e_call) { this.__callback({ "obj" : obj }); }, _is_loaded : function (obj) { return true; }, // Basic operations: create create_node : function (obj, position, js, callback, is_loaded) { obj = this._get_node(obj); position = typeof position === "undefined" ? "last" : position; var d = $("<li />"), s = this._get_settings().core, tmp; if(obj !== -1 && !obj.length) { return false; } if(!is_loaded && !this._is_loaded(obj)) { this.load_node(obj, function () { this.create_node(obj, position, js, callback, true); }); return false; } this.__rollback(); if(typeof js === "string") { js = { "data" : js }; } if(!js) { js = {}; } if(js.attr) { d.attr(js.attr); } if(js.metadata) { d.data(js.metadata); } if(js.state) { d.addClass("jstree-" + js.state); } if(!js.data) { js.data = this._get_string("new_node"); } if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } $.each(js.data, function (i, m) { tmp = $("<a />"); if($.isFunction(m)) { m = m.call(this, js); } if(typeof m == "string") { tmp.attr('href','#')[ s.html_titles ? "html" : "text" ](m); } else { if(!m.attr) { m.attr = {}; } if(!m.attr.href) { m.attr.href = '#'; } tmp.attr(m.attr)[ s.html_titles ? "html" : "text" ](m.title); if(m.language) { tmp.addClass(m.language); } } tmp.prepend("<ins class='jstree-icon'> </ins>"); if(!m.icon && js.icon) { m.icon = js.icon; } if(m.icon) { if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } } d.append(tmp); }); d.prepend("<ins class='jstree-icon'> </ins>"); if(obj === -1) { obj = this.get_container(); if(position === "before") { position = "first"; } if(position === "after") { position = "last"; } } switch(position) { case "before": obj.before(d); tmp = this._get_parent(obj); break; case "after" : obj.after(d); tmp = this._get_parent(obj); break; case "inside": case "first" : if(!obj.children("ul").length) { obj.append("<ul />"); } obj.children("ul").prepend(d); tmp = obj; break; case "last": if(!obj.children("ul").length) { obj.append("<ul />"); } obj.children("ul").append(d); tmp = obj; break; default: if(!obj.children("ul").length) { obj.append("<ul />"); } if(!position) { position = 0; } tmp = obj.children("ul").children("li").eq(position); if(tmp.length) { tmp.before(d); } else { obj.children("ul").append(d); } tmp = obj; break; } if(tmp === -1 || tmp.get(0) === this.get_container().get(0)) { tmp = -1; } this.clean_node(tmp); this.__callback({ "obj" : d, "parent" : tmp }); if(callback) { callback.call(this, d); } return d; }, // Basic operations: rename (deal with text) get_text : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } var s = this._get_settings().core.html_titles; obj = obj.children("a:eq(0)"); if(s) { obj = obj.clone(); obj.children("INS").remove(); return obj.html(); } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; return obj.nodeValue; } }, set_text : function (obj, val) { obj = this._get_node(obj); if(!obj.length) { return false; } obj = obj.children("a:eq(0)"); if(this._get_settings().core.html_titles) { var tmp = obj.children("INS").clone(); obj.html(val).prepend(tmp); this.__callback({ "obj" : obj, "name" : val }); return true; } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; this.__callback({ "obj" : obj, "name" : val }); return (obj.nodeValue = val); } }, rename_node : function (obj, val) { obj = this._get_node(obj); this.__rollback(); if(obj && obj.length && this.set_text.apply(this, Array.prototype.slice.call(arguments))) { this.__callback({ "obj" : obj, "name" : val }); } }, // Basic operations: deleting nodes delete_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } this.__rollback(); var p = this._get_parent(obj), prev = $([]), t = this; obj.each(function () { prev = prev.add(t._get_prev(this)); }); obj = obj.detach(); if(p !== -1 && p.find("> ul > li").length === 0) { p.removeClass("jstree-open jstree-closed").addClass("jstree-leaf"); } this.clean_node(p); this.__callback({ "obj" : obj, "prev" : prev, "parent" : p }); return obj; }, prepare_move : function (o, r, pos, cb, is_cb) { var p = {}; p.ot = $.jstree._reference(o) || this; p.o = p.ot._get_node(o); p.r = r === - 1 ? -1 : this._get_node(r); p.p = (typeof pos === "undefined" || pos === false) ? "last" : pos; // TODO: move to a setting if(!is_cb && prepared_move.o && prepared_move.o[0] === p.o[0] && prepared_move.r[0] === p.r[0] && prepared_move.p === p.p) { this.__callback(prepared_move); if(cb) { cb.call(this, prepared_move); } return; } p.ot = $.jstree._reference(p.o) || this; p.rt = $.jstree._reference(p.r) || this; // r === -1 ? p.ot : $.jstree._reference(p.r) || this if(p.r === -1 || !p.r) { p.cr = -1; switch(p.p) { case "first": case "before": case "inside": p.cp = 0; break; case "after": case "last": p.cp = p.rt.get_container().find(" > ul > li").length; break; default: p.cp = p.p; break; } } else { if(!/^(before|after)$/.test(p.p) && !this._is_loaded(p.r)) { return this.load_node(p.r, function () { this.prepare_move(o, r, pos, cb, true); }); } switch(p.p) { case "before": p.cp = p.r.index(); p.cr = p.rt._get_parent(p.r); break; case "after": p.cp = p.r.index() + 1; p.cr = p.rt._get_parent(p.r); break; case "inside": case "first": p.cp = 0; p.cr = p.r; break; case "last": p.cp = p.r.find(" > ul > li").length; p.cr = p.r; break; default: p.cp = p.p; p.cr = p.r; break; } } p.np = p.cr == -1 ? p.rt.get_container() : p.cr; p.op = p.ot._get_parent(p.o); p.cop = p.o.index(); if(p.op === -1) { p.op = p.ot ? p.ot.get_container() : this.get_container(); } if(!/^(before|after)$/.test(p.p) && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp++; } //if(p.p === "before" && p.op && p.np && p.op[0] === p.np[0] && p.o.index() < p.cp) { p.cp--; } p.or = p.np.find(" > ul > li:nth-child(" + (p.cp + 1) + ")"); prepared_move = p; this.__callback(prepared_move); if(cb) { cb.call(this, prepared_move); } }, check_move : function () { var obj = prepared_move, ret = true, r = obj.r === -1 ? this.get_container() : obj.r; if(!obj || !obj.o || obj.or[0] === obj.o[0]) { return false; } if(obj.op && obj.np && obj.op[0] === obj.np[0] && obj.cp - 1 === obj.o.index()) { return false; } obj.o.each(function () { if(r.parentsUntil(".jstree", "li").andSelf().index(this) !== -1) { ret = false; return false; } }); return ret; }, move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) { if(!is_prepared) { return this.prepare_move(obj, ref, position, function (p) { this.move_node(p, false, false, is_copy, true, skip_check); }); } if(is_copy) { prepared_move.cy = true; } if(!skip_check && !this.check_move()) { return false; } this.__rollback(); var o = false; if(is_copy) { o = obj.o.clone(true); o.find("*[id]").andSelf().each(function () { if(this.id) { this.id = "copy_" + this.id; } }); } else { o = obj.o; } if(obj.or.length) { obj.or.before(o); } else { if(!obj.np.children("ul").length) { $("<ul />").appendTo(obj.np); } obj.np.children("ul:eq(0)").append(o); } try { obj.ot.clean_node(obj.op); obj.rt.clean_node(obj.np); if(!obj.op.find("> ul > li").length) { obj.op.removeClass("jstree-open jstree-closed").addClass("jstree-leaf").children("ul").remove(); } } catch (e) { } if(is_copy) { prepared_move.cy = true; prepared_move.oc = o; } this.__callback(prepared_move); return prepared_move; }, _get_move : function () { return prepared_move; } } }); })(jQuery); //*/ /* * jsTree ui plugin * This plugins handles selecting/deselecting/hovering/dehovering nodes */ (function ($) { var scrollbar_width, e1, e2; $(function() { if (/msie/.test(navigator.userAgent.toLowerCase())) { e1 = $('<textarea cols="10" rows="2"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body'); e2 = $('<textarea cols="10" rows="2" style="overflow: hidden;"></textarea>').css({ position: 'absolute', top: -1000, left: 0 }).appendTo('body'); scrollbar_width = e1.width() - e2.width(); e1.add(e2).remove(); } else { e1 = $('<div />').css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: 0 }) .prependTo('body').append('<div />').find('div').css({ width: '100%', height: 200 }); scrollbar_width = 100 - e1.width(); e1.parent().remove(); } }); $.jstree.plugin("ui", { __init : function () { this.data.ui.selected = $(); this.data.ui.last_selected = false; this.data.ui.hovered = null; this.data.ui.to_select = this.get_settings().ui.initially_select; this.get_container() .delegate("a", "click.jstree", $.proxy(function (event) { event.preventDefault(); event.currentTarget.blur(); if(!$(event.currentTarget).hasClass("jstree-loading")) { this.select_node(event.currentTarget, true, event); } }, this)) .delegate("a", "mouseenter.jstree", $.proxy(function (event) { if(!$(event.currentTarget).hasClass("jstree-loading")) { this.hover_node(event.target); } }, this)) .delegate("a", "mouseleave.jstree", $.proxy(function (event) { if(!$(event.currentTarget).hasClass("jstree-loading")) { this.dehover_node(event.target); } }, this)) .bind("reopen.jstree", $.proxy(function () { this.reselect(); }, this)) .bind("get_rollback.jstree", $.proxy(function () { this.dehover_node(); this.save_selected(); }, this)) .bind("set_rollback.jstree", $.proxy(function () { this.reselect(); }, this)) .bind("close_node.jstree", $.proxy(function (event, data) { var s = this._get_settings().ui, obj = this._get_node(data.rslt.obj), clk = (obj && obj.length) ? obj.children("ul").find("a.jstree-clicked") : $(), _this = this; if(s.selected_parent_close === false || !clk.length) { return; } clk.each(function () { _this.deselect_node(this); if(s.selected_parent_close === "select_parent") { _this.select_node(obj); } }); }, this)) .bind("delete_node.jstree", $.proxy(function (event, data) { var s = this._get_settings().ui.select_prev_on_delete, obj = this._get_node(data.rslt.obj), clk = (obj && obj.length) ? obj.find("a.jstree-clicked") : [], _this = this; clk.each(function () { _this.deselect_node(this); }); if(s && clk.length) { data.rslt.prev.each(function () { if(this.parentNode) { _this.select_node(this); return false; /* if return false is removed all prev nodes will be selected */} }); } }, this)) .bind("move_node.jstree", $.proxy(function (event, data) { if(data.rslt.cy) { data.rslt.oc.find("a.jstree-clicked").removeClass("jstree-clicked"); } }, this)); }, defaults : { select_limit : -1, // 0, 1, 2 ... or -1 for unlimited select_multiple_modifier : "ctrl", // on, or ctrl, shift, alt select_range_modifier : "shift", selected_parent_close : "select_parent", // false, "deselect", "select_parent" selected_parent_open : true, select_prev_on_delete : true, disable_selecting_children : false, initially_select : [] }, _fn : { _get_node : function (obj, allow_multiple) { if(typeof obj === "undefined" || obj === null) { return allow_multiple ? this.data.ui.selected : this.data.ui.last_selected; } var $obj = $(obj, this.get_container()); if($obj.is(".jstree") || obj == -1) { return -1; } $obj = $obj.closest("li", this.get_container()); return $obj.length ? $obj : false; }, _ui_notify : function (n, data) { if(data.selected) { this.select_node(n, false); } }, save_selected : function () { var _this = this; this.data.ui.to_select = []; this.data.ui.selected.each(function () { if(this.id) { _this.data.ui.to_select.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } }); this.__callback(this.data.ui.to_select); }, reselect : function () { var _this = this, s = this.data.ui.to_select; s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); // this.deselect_all(); WHY deselect, breaks plugin state notifier? $.each(s, function (i, val) { if(val && val !== "#") { _this.select_node(val); } }); this.data.ui.selected = this.data.ui.selected.filter(function () { return this.parentNode; }); this.__callback(); }, refresh : function (obj) { this.save_selected(); return this.__call_old(); }, hover_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } //if(this.data.ui.hovered && obj.get(0) === this.data.ui.hovered.get(0)) { return; } if(!obj.hasClass("jstree-hovered")) { this.dehover_node(); } this.data.ui.hovered = obj.children("a").addClass("jstree-hovered").parent(); // brackets: jstree uses default scroll bar widths, and there's no clean way to override the code from brackets, which causes continuous scrolling, so just disable for now //this._fix_scroll(obj); this.__callback({ "obj" : obj }); }, dehover_node : function () { var obj = this.data.ui.hovered, p; if(!obj || !obj.length) { return false; } p = obj.children("a").removeClass("jstree-hovered").parent(); if(this.data.ui.hovered[0] === p[0]) { this.data.ui.hovered = null; } this.__callback({ "obj" : obj }); }, select_node : function (obj, check, e) { obj = this._get_node(obj); if(obj == -1 || !obj || !obj.length) { return false; } var s = this._get_settings().ui, is_multiple = (s.select_multiple_modifier == "on" || (s.select_multiple_modifier !== false && e && e[s.select_multiple_modifier + "Key"])), is_range = (s.select_range_modifier !== false && e && e[s.select_range_modifier + "Key"] && this.data.ui.last_selected && this.data.ui.last_selected[0] !== obj[0] && this.data.ui.last_selected.parent()[0] === obj.parent()[0]), is_selected = this.is_selected(obj), proceed = true, t = this; if(check) { if(s.disable_selecting_children && is_multiple && ( (obj.parentsUntil(".jstree","li").children("a.jstree-clicked").length) || (obj.children("ul").find("a.jstree-clicked:eq(0)").length) ) ) { return false; } proceed = false; switch(!0) { case (is_range): this.data.ui.last_selected.addClass("jstree-last-selected"); obj = obj[ obj.index() < this.data.ui.last_selected.index() ? "nextUntil" : "prevUntil" ](".jstree-last-selected").andSelf(); if(s.select_limit == -1 || obj.length < s.select_limit) { this.data.ui.last_selected.removeClass("jstree-last-selected"); this.data.ui.selected.each(function () { if(this !== t.data.ui.last_selected[0]) { t.deselect_node(this); } }); is_selected = false; proceed = true; } else { proceed = false; } break; case (is_selected && !is_multiple): this.deselect_all(); is_selected = false; proceed = true; break; case (!is_selected && !is_multiple): if(s.select_limit == -1 || s.select_limit > 0) { this.deselect_all(); proceed = true; } break; case (is_selected && is_multiple): this.deselect_node(obj); break; case (!is_selected && is_multiple): if(s.select_limit == -1 || this.data.ui.selected.length + 1 <= s.select_limit) { proceed = true; } break; } } if(proceed && !is_selected) { if(!is_range) { this.data.ui.last_selected = obj; } obj.children("a").addClass("jstree-clicked"); if(s.selected_parent_open) { obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); } this.data.ui.selected = this.data.ui.selected.add(obj); this._fix_scroll(obj.eq(0)); this.__callback({ "obj" : obj, "e" : e }); } }, _fix_scroll : function (obj) { var c = this.get_container()[0], t; if(c.scrollHeight > c.offsetHeight) { obj = this._get_node(obj); if(!obj || obj === -1 || !obj.length || !obj.is(":visible")) { return; } t = obj.offset().top - this.get_container().offset().top; if(t < 0) { c.scrollTop = c.scrollTop + t - 1; } if(t + this.data.core.li_height + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0) > c.offsetHeight) { c.scrollTop = c.scrollTop + (t - c.offsetHeight + this.data.core.li_height + 1 + (c.scrollWidth > c.offsetWidth ? scrollbar_width : 0)); } } }, deselect_node : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } if(this.is_selected(obj)) { obj.children("a").removeClass("jstree-clicked"); this.data.ui.selected = this.data.ui.selected.not(obj); if(this.data.ui.last_selected.get(0) === obj.get(0)) { this.data.ui.last_selected = this.data.ui.selected.eq(0); } this.__callback({ "obj" : obj }); } }, toggle_select : function (obj) { obj = this._get_node(obj); if(!obj.length) { return false; } if(this.is_selected(obj)) { this.deselect_node(obj); } else { this.select_node(obj); } }, is_selected : function (obj) { return this.data.ui.selected.index(this._get_node(obj)) >= 0; }, get_selected : function (context) { return context ? $(context).find("a.jstree-clicked").parent() : this.data.ui.selected; }, deselect_all : function (context) { var ret = context ? $(context).find("a.jstree-clicked").parent() : this.get_container().find("a.jstree-clicked").parent(); ret.children("a.jstree-clicked").removeClass("jstree-clicked"); this.data.ui.selected = $([]); this.data.ui.last_selected = false; this.__callback({ "obj" : ret }); } } }); // include the selection plugin by default $.jstree.defaults.plugins.push("ui"); })(jQuery); //*/ /* * jsTree CRRM plugin * Handles creating/renaming/removing/moving nodes by user interaction. */ (function ($) { $.jstree.plugin("crrm", { __init : function () { this.get_container() .bind("move_node.jstree", $.proxy(function (e, data) { if(this._get_settings().crrm.move.open_onmove) { var t = this; data.rslt.np.parentsUntil(".jstree").andSelf().filter(".jstree-closed").each(function () { t.open_node(this, false, true); }); } }, this)); }, defaults : { input_width_limit : 200, move : { always_copy : false, // false, true or "multitree" open_onmove : true, default_position : "last", check_move : function (m) { return true; } } }, _fn : { _show_input : function (obj, callback) { obj = this._get_node(obj); var rtl = this._get_settings().core.rtl, w = this._get_settings().crrm.input_width_limit, w1 = obj.children("ins").width(), w2 = obj.find("> a:visible > ins").width() * obj.find("> a:visible > ins").length, t = this.get_text(obj), h1 = $("<div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"), h2 = obj.css("position","relative").append( $("<input />", { "value" : t, "class" : "jstree-rename-input", // "size" : t.length, "css" : { "padding" : "0", "border" : "1px solid silver", "position" : "absolute", "left" : (rtl ? "auto" : (w1 + w2 + 4) + "px"), "right" : (rtl ? (w1 + w2 + 4) + "px" : "auto"), "top" : "0px", "height" : (this.data.core.li_height - 2) + "px", "lineHeight" : (this.data.core.li_height - 2) + "px", "width" : "150px" // will be set a bit further down }, "blur" : $.proxy(function () { var i = obj.children(".jstree-rename-input"), v = i.val(); if(v === "") { v = t; } h1.remove(); i.remove(); // rollback purposes this.set_text(obj,t); // rollback purposes this.rename_node(obj, v); callback.call(this, obj, v, t); obj.css("position",""); }, this), "keyup" : function (event) { var key = event.keyCode || event.which; if(key == 27) { this.value = t; this.blur(); return; } else if(key == 13) { this.blur(); return; } else { h2.width(Math.min(h1.text("pW" + this.value).width(),w)); } }, "keypress" : function(event) { var key = event.keyCode || event.which; if(key == 13) { return false; } } }) ).children(".jstree-rename-input"); this.set_text(obj, ""); h1.css({ fontFamily : h2.css('fontFamily') || '', fontSize : h2.css('fontSize') || '', fontWeight : h2.css('fontWeight') || '', fontStyle : h2.css('fontStyle') || '', fontStretch : h2.css('fontStretch') || '', fontVariant : h2.css('fontVariant') || '', letterSpacing : h2.css('letterSpacing') || '', wordSpacing : h2.css('wordSpacing') || '' }); h2.width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); }, rename : function (obj) { obj = this._get_node(obj); this.__rollback(); var f = this.__callback; this._show_input(obj, function (obj, new_name, old_name) { f.call(this, { "obj" : obj, "new_name" : new_name, "old_name" : old_name }); }); }, create : function (obj, position, js, callback, skip_rename) { var t, _this = this; obj = this._get_node(obj); if(!obj) { obj = -1; } this.__rollback(); t = this.create_node(obj, position, js, function (t) { var p = this._get_parent(t), pos = $(t).index(); if(callback) { callback.call(this, t); } if(p.length && p.hasClass("jstree-closed")) { this.open_node(p, false, true); } if(!skip_rename) { this._show_input(t, function (obj, new_name, old_name) { _this.__callback({ "obj" : obj, "name" : new_name, "parent" : p, "position" : pos }); }); } else { _this.__callback({ "obj" : t, "name" : this.get_text(t), "parent" : p, "position" : pos }); } }); return t; }, remove : function (obj) { obj = this._get_node(obj, true); var p = this._get_parent(obj), prev = this._get_prev(obj); this.__rollback(); obj = this.delete_node(obj); if(obj !== false) { this.__callback({ "obj" : obj, "prev" : prev, "parent" : p }); } }, check_move : function () { if(!this.__call_old()) { return false; } var s = this._get_settings().crrm.move; if(!s.check_move.call(this, this._get_move())) { return false; } return true; }, move_node : function (obj, ref, position, is_copy, is_prepared, skip_check) { var s = this._get_settings().crrm.move; if(!is_prepared) { if(typeof position === "undefined") { position = s.default_position; } if(position === "inside" && !s.default_position.match(/^(before|after)$/)) { position = s.default_position; } return this.__call_old(true, obj, ref, position, is_copy, false, skip_check); } // if the move is already prepared if(s.always_copy === true || (s.always_copy === "multitree" && obj.rt.get_index() !== obj.ot.get_index() )) { is_copy = true; } this.__call_old(true, obj, ref, position, is_copy, true, skip_check); }, cut : function (obj) { obj = this._get_node(obj, true); if(!obj || !obj.length) { return false; } this.data.crrm.cp_nodes = false; this.data.crrm.ct_nodes = obj; this.__callback({ "obj" : obj }); }, copy : function (obj) { obj = this._get_node(obj, true); if(!obj || !obj.length) { return false; } this.data.crrm.ct_nodes = false; this.data.crrm.cp_nodes = obj; this.__callback({ "obj" : obj }); }, paste : function (obj) { obj = this._get_node(obj); if(!obj || !obj.length) { return false; } var nodes = this.data.crrm.ct_nodes ? this.data.crrm.ct_nodes : this.data.crrm.cp_nodes; if(!this.data.crrm.ct_nodes && !this.data.crrm.cp_nodes) { return false; } if(this.data.crrm.ct_nodes) { this.move_node(this.data.crrm.ct_nodes, obj); this.data.crrm.ct_nodes = false; } if(this.data.crrm.cp_nodes) { this.move_node(this.data.crrm.cp_nodes, obj, false, true); } this.__callback({ "obj" : obj, "nodes" : nodes }); } } }); // include the crr plugin by default // $.jstree.defaults.plugins.push("crrm"); })(jQuery); //*/ /* * jsTree themes plugin * Handles loading and setting themes, as well as detecting path to themes, etc. */ (function ($) { var themes_loaded = []; // this variable stores the path to the themes folder - if left as false - it will be autodetected $.jstree._themes = false; $.jstree.plugin("themes", { __init : function () { this.get_container() .bind("init.jstree", $.proxy(function () { var s = this._get_settings().themes; this.data.themes.dots = s.dots; this.data.themes.icons = s.icons; this.set_theme(s.theme, s.url); }, this)) .bind("loaded.jstree", $.proxy(function () { // bound here too, as simple HTML tree's won't honor dots & icons otherwise if(!this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } if(!this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } }, this)); }, defaults : { theme : "default", url : false, dots : true, icons : true }, _fn : { set_theme : function (theme_name, theme_url) { if(!theme_name) { return false; } if(!theme_url) { theme_url = $.jstree._themes + theme_name + '/style.css'; } if($.inArray(theme_url, themes_loaded) == -1) { $.vakata.css.add_sheet({ "url" : theme_url }); themes_loaded.push(theme_url); } if(this.data.themes.theme != theme_name) { this.get_container().removeClass('jstree-' + this.data.themes.theme); this.data.themes.theme = theme_name; } this.get_container().addClass('jstree-' + theme_name); if(!this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } if(!this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } this.__callback(); }, get_theme : function () { return this.data.themes.theme; }, show_dots : function () { this.data.themes.dots = true; this.get_container().children("ul").removeClass("jstree-no-dots"); }, hide_dots : function () { this.data.themes.dots = false; this.get_container().children("ul").addClass("jstree-no-dots"); }, toggle_dots : function () { if(this.data.themes.dots) { this.hide_dots(); } else { this.show_dots(); } }, show_icons : function () { this.data.themes.icons = true; this.get_container().children("ul").removeClass("jstree-no-icons"); }, hide_icons : function () { this.data.themes.icons = false; this.get_container().children("ul").addClass("jstree-no-icons"); }, toggle_icons: function () { if(this.data.themes.icons) { this.hide_icons(); } else { this.show_icons(); } } } }); // autodetect themes path $(function () { if($.jstree._themes === false) { $("script").each(function () { if(this.src.toString().match(/jquery\.jstree[^\/]*?\.js(\?.*)?$/)) { $.jstree._themes = this.src.toString().replace(/jquery\.jstree[^\/]*?\.js(\?.*)?$/, "") + 'themes/'; return false; } }); } if($.jstree._themes === false) { $.jstree._themes = "themes/"; } }); // include the themes plugin by default $.jstree.defaults.plugins.push("themes"); })(jQuery); //*/ /* * jsTree hotkeys plugin * Enables keyboard navigation for all tree instances * Depends on the jstree ui & jquery hotkeys plugins */ (function ($) { var bound = []; function exec(i, event) { var f = $.jstree._focused(), tmp; if(f && f.data && f.data.hotkeys && f.data.hotkeys.enabled) { tmp = f._get_settings().hotkeys[i]; if(tmp) { return tmp.call(f, event); } } } $.jstree.plugin("hotkeys", { __init : function () { if(typeof $.hotkeys === "undefined") { throw "jsTree hotkeys: jQuery hotkeys plugin not included."; } if(!this.data.ui) { throw "jsTree hotkeys: jsTree UI plugin not included."; } $.each(this._get_settings().hotkeys, function (i, v) { if(v !== false && $.inArray(i, bound) == -1) { $(document).bind("keydown", i, function (event) { return exec(i, event); }); bound.push(i); } }); this.get_container() .bind("lock.jstree", $.proxy(function () { if(this.data.hotkeys.enabled) { this.data.hotkeys.enabled = false; this.data.hotkeys.revert = true; } }, this)) .bind("unlock.jstree", $.proxy(function () { if(this.data.hotkeys.revert) { this.data.hotkeys.enabled = true; } }, this)); this.enable_hotkeys(); }, defaults : { "up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "ctrl+up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "shift+up" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_prev(o)); return false; }, "down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "ctrl+down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "shift+down" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected || -1; this.hover_node(this._get_next(o)); return false; }, "left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "ctrl+left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "shift+left" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o) { if(o.hasClass("jstree-open")) { this.close_node(o); } else { this.hover_node(this._get_prev(o)); } } return false; }, "right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "ctrl+right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "shift+right" : function () { var o = this.data.ui.hovered || this.data.ui.last_selected; if(o && o.length) { if(o.hasClass("jstree-closed")) { this.open_node(o); } else { this.hover_node(this._get_next(o)); } } return false; }, "space" : function () { if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").click(); } return false; }, "ctrl+space" : function (event) { event.type = "click"; if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); } return false; }, "shift+space" : function (event) { event.type = "click"; if(this.data.ui.hovered) { this.data.ui.hovered.children("a:eq(0)").trigger(event); } return false; }, "f2" : function () { this.rename(this.data.ui.hovered || this.data.ui.last_selected); }, "del" : function () { this.remove(this.data.ui.hovered || this._get_node(null)); } }, _fn : { enable_hotkeys : function () { this.data.hotkeys.enabled = true; }, disable_hotkeys : function () { this.data.hotkeys.enabled = false; } } }); })(jQuery); //*/ /* * jsTree JSON plugin * The JSON data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions. */ (function ($) { $.jstree.plugin("json_data", { __init : function() { var s = this._get_settings().json_data; if(s.progressive_unload) { this.get_container().bind("after_close.jstree", function (e, data) { data.rslt.obj.children("ul").remove(); }); } }, defaults : { // `data` can be a function: // * accepts two arguments - node being loaded and a callback to pass the result to // * will be executed in the current tree's scope & ajax won't be supported data : false, ajax : false, correct_state : true, progressive_render : false, progressive_unload : false }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_json(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { var s = this._get_settings().json_data; obj = this._get_node(obj); return obj == -1 || !obj || (!s.ajax && !s.progressive_render && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").length > 0; }, refresh : function (obj) { obj = this._get_node(obj); var s = this._get_settings().json_data; if(obj && obj !== -1 && s.progressive_unload && ($.isFunction(s.data) || !!s.ajax)) { obj.removeData("jstree_children"); } return this.__call_old(); }, load_node_json : function (obj, s_call, e_call) { var s = this.get_settings().json_data, d, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1 && (s.progressive_render || s.progressive_unload) && !obj.is(".jstree-open, .jstree-leaf") && obj.children("ul").children("li").length === 0 && obj.data("jstree_children")) { d = this._parse_json(obj.data("jstree_children"), obj); if(d) { obj.append(d); if(!s.progressive_unload) { obj.removeData("jstree_children"); } } this.clean_node(obj); if(s_call) { s_call.call(this); } return; } if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied."; // function option added here for easier model integration (also supporting async - see callback) case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { d = this._parse_json(d, obj); if(!d) { if(obj === -1 || !obj) { if(s.correct_state) { this.get_container().children("ul").empty(); } } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); } } if(e_call) { e_call.call(this); } } else { if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } }, this)); break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { d = this._parse_json(s.data, obj); if(d) { this.get_container().children("ul").empty().append(d.children()); this.clean_node(); } else { if(s.correct_state) { this.get_container().children("ul").empty(); } } } if(s_call) { s_call.call(this); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): error_func = function (x, t, e) { var ef = this.get_settings().json_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj != -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { var sf = this.get_settings().json_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "") || (!$.isArray(d) && !$.isPlainObject(d))) { return error_func.call(this, x, t, ""); } d = this._parse_json(d, obj); if(d) { if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.append(d).children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj === -1 || !obj) { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } } }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "json"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } }, _parse_json : function (js, obj, is_callback) { var d = false, p = this._get_settings(), s = p.json_data, t = p.core.html_titles, tmp, i, j, ul1, ul2; if(!js) { return d; } if(s.progressive_unload && obj && obj !== -1) { obj.data("jstree_children", d); } if($.isArray(js)) { d = $(); if(!js.length) { return false; } for(i = 0, j = js.length; i < j; i++) { tmp = this._parse_json(js[i], obj, true); if(tmp.length) { d = d.add(tmp); } } } else { if(typeof js == "string") { js = { data : js }; } if(!js.data && js.data !== "") { return d; } d = $("<li />"); if(js.attr) { d.attr(js.attr); } if(js.metadata) { d.data(js.metadata); } if(js.state) { d.addClass("jstree-" + js.state); } if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } $.each(js.data, function (i, m) { tmp = $("<a />"); if($.isFunction(m)) { m = m.call(this, js); } if(typeof m == "string") { tmp.attr('href','#')[ t ? "html" : "text" ](m); } else { if(!m.attr) { m.attr = {}; } if(!m.attr.href) { m.attr.href = '#'; } tmp.attr(m.attr)[ t ? "html" : "text" ](m.title); if(m.language) { tmp.addClass(m.language); } } tmp.prepend("<ins class='jstree-icon'> </ins>"); if(!m.icon && js.icon) { m.icon = js.icon; } if(m.icon) { if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } } d.append(tmp); }); d.prepend("<ins class='jstree-icon'> </ins>"); if(js.children) { if(s.progressive_render && js.state !== "open") { d.addClass("jstree-closed").data("jstree_children", js.children); } else { if(s.progressive_unload) { d.data("jstree_children", js.children); } if($.isArray(js.children) && js.children.length) { tmp = this._parse_json(js.children, obj, true); if(tmp.length) { ul2 = $("<ul />"); ul2.append(tmp); d.append(ul2); } } } } } if(!is_callback) { ul1 = $("<ul />"); ul1.append(d); d = ul1; } return d; }, get_json : function (obj, li_attr, a_attr, is_callback) { var result = [], s = this._get_settings(), _this = this, tmp1, tmp2, li, a, t, lang; obj = this._get_node(obj); if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); } li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ]; if(!is_callback && this.data.types) { li_attr.push(s.types.type_attr); } a_attr = $.isArray(a_attr) ? a_attr : [ ]; obj.each(function () { li = $(this); tmp1 = { data : [] }; if(li_attr.length) { tmp1.attr = { }; } $.each(li_attr, function (i, v) { tmp2 = li.attr(v); if(tmp2 && tmp2.length && tmp2.replace(/jstree[^ ]*/ig,'').length) { tmp1.attr[v] = (" " + tmp2).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); } }); if(li.hasClass("jstree-open")) { tmp1.state = "open"; } if(li.hasClass("jstree-closed")) { tmp1.state = "closed"; } if(li.data()) { tmp1.metadata = li.data(); } a = li.children("a"); a.each(function () { t = $(this); if( a_attr.length || $.inArray("languages", s.plugins) !== -1 || t.children("ins").get(0).style.backgroundImage.length || (t.children("ins").get(0).className && t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').length) ) { lang = false; if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) { $.each(s.languages, function (l, lv) { if(t.hasClass(lv)) { lang = lv; return false; } }); } tmp2 = { attr : { }, title : _this.get_text(t, lang) }; $.each(a_attr, function (k, z) { tmp2.attr[z] = (" " + (t.attr(z) || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); }); if($.inArray("languages", s.plugins) !== -1 && $.isArray(s.languages) && s.languages.length) { $.each(s.languages, function (k, z) { if(t.hasClass(z)) { tmp2.language = z; return true; } }); } if(t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) { tmp2.icon = t.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,""); } if(t.children("ins").get(0).style.backgroundImage.length) { tmp2.icon = t.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")",""); } } else { tmp2 = _this.get_text(t); } if(a.length > 1) { tmp1.data.push(tmp2); } else { tmp1.data = tmp2; } }); li = li.find("> ul > li"); if(li.length) { tmp1.children = _this.get_json(li, li_attr, a_attr, true); } result.push(tmp1); }); return result; } } }); })(jQuery); //*/ /* * jsTree languages plugin * Adds support for multiple language versions in one tree * This basically allows for many titles coexisting in one node, but only one of them being visible at any given time * This is useful for maintaining the same structure in many languages (hence the name of the plugin) */ (function ($) { $.jstree.plugin("languages", { __init : function () { this._load_css(); }, defaults : [], _fn : { set_lang : function (i) { var langs = this._get_settings().languages, st = false, selector = ".jstree-" + this.get_index() + ' a'; if(!$.isArray(langs) || langs.length === 0) { return false; } if($.inArray(i,langs) == -1) { if(!!langs[i]) { i = langs[i]; } else { return false; } } if(i == this.data.languages.current_language) { return true; } st = $.vakata.css.get_css(selector + "." + this.data.languages.current_language, false, this.data.languages.language_css); if(st !== false) { st.style.display = "none"; } st = $.vakata.css.get_css(selector + "." + i, false, this.data.languages.language_css); if(st !== false) { st.style.display = ""; } this.data.languages.current_language = i; this.__callback(i); return true; }, get_lang : function () { return this.data.languages.current_language; }, _get_string : function (key, lang) { var langs = this._get_settings().languages, s = this._get_settings().core.strings; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; } if(s[lang] && s[lang][key]) { return s[lang][key]; } if(s[key]) { return s[key]; } return key; }, get_text : function (obj, lang) { obj = this._get_node(obj) || this.data.ui.last_selected; if(!obj.size()) { return false; } var langs = this._get_settings().languages, s = this._get_settings().core.html_titles; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; obj = obj.children("a." + lang); } else { obj = obj.children("a:eq(0)"); } if(s) { obj = obj.clone(); obj.children("INS").remove(); return obj.html(); } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; return obj.nodeValue; } }, set_text : function (obj, val, lang) { obj = this._get_node(obj) || this.data.ui.last_selected; if(!obj.size()) { return false; } var langs = this._get_settings().languages, s = this._get_settings().core.html_titles, tmp; if($.isArray(langs) && langs.length) { lang = (lang && $.inArray(lang,langs) != -1) ? lang : this.data.languages.current_language; obj = obj.children("a." + lang); } else { obj = obj.children("a:eq(0)"); } if(s) { tmp = obj.children("INS").clone(); obj.html(val).prepend(tmp); this.__callback({ "obj" : obj, "name" : val, "lang" : lang }); return true; } else { obj = obj.contents().filter(function() { return this.nodeType == 3; })[0]; this.__callback({ "obj" : obj, "name" : val, "lang" : lang }); return (obj.nodeValue = val); } }, _load_css : function () { var langs = this._get_settings().languages, str = "/* languages css */", selector = ".jstree-" + this.get_index() + ' a', ln; if($.isArray(langs) && langs.length) { this.data.languages.current_language = langs[0]; for(ln = 0; ln < langs.length; ln++) { str += selector + "." + langs[ln] + " {"; if(langs[ln] != this.data.languages.current_language) { str += " display:none; "; } str += " } "; } this.data.languages.language_css = $.vakata.css.add_sheet({ 'str' : str, 'title' : "jstree-languages" }); } }, create_node : function (obj, position, js, callback) { var t = this.__call_old(true, obj, position, js, function (t) { var langs = this._get_settings().languages, a = t.children("a"), ln; if($.isArray(langs) && langs.length) { for(ln = 0; ln < langs.length; ln++) { if(!a.is("." + langs[ln])) { t.append(a.eq(0).clone().removeClass(langs.join(" ")).addClass(langs[ln])); } } a.not("." + langs.join(", .")).remove(); } if(callback) { callback.call(this, t); } }); return t; } } }); })(jQuery); //*/ /* * jsTree cookies plugin * Stores the currently opened/selected nodes in a cookie and then restores them * Depends on the jquery.cookie plugin */ (function ($) { $.jstree.plugin("cookies", { __init : function () { if(typeof $.cookie === "undefined") { throw "jsTree cookie: jQuery cookie plugin not included."; } var s = this._get_settings().cookies, tmp; if(!!s.save_loaded) { tmp = $.cookie(s.save_loaded); if(tmp && tmp.length) { this.data.core.to_load = tmp.split(","); } } if(!!s.save_opened) { tmp = $.cookie(s.save_opened); if(tmp && tmp.length) { this.data.core.to_open = tmp.split(","); } } if(!!s.save_selected) { tmp = $.cookie(s.save_selected); if(tmp && tmp.length && this.data.ui) { this.data.ui.to_select = tmp.split(","); } } this.get_container() .one( ( this.data.ui ? "reselect" : "reopen" ) + ".jstree", $.proxy(function () { this.get_container() .bind("open_node.jstree close_node.jstree select_node.jstree deselect_node.jstree", $.proxy(function (e) { if(this._get_settings().cookies.auto_save) { this.save_cookie((e.handleObj.namespace + e.handleObj.type).replace("jstree","")); } }, this)); }, this)); }, defaults : { save_loaded : "jstree_load", save_opened : "jstree_open", save_selected : "jstree_select", auto_save : true, cookie_options : {} }, _fn : { save_cookie : function (c) { if(this.data.core.refreshing) { return; } var s = this._get_settings().cookies; if(!c) { // if called manually and not by event if(s.save_loaded) { this.save_loaded(); $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options); } if(s.save_opened) { this.save_opened(); $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options); } if(s.save_selected && this.data.ui) { this.save_selected(); $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options); } return; } switch(c) { case "open_node": case "close_node": if(!!s.save_opened) { this.save_opened(); $.cookie(s.save_opened, this.data.core.to_open.join(","), s.cookie_options); } if(!!s.save_loaded) { this.save_loaded(); $.cookie(s.save_loaded, this.data.core.to_load.join(","), s.cookie_options); } break; case "select_node": case "deselect_node": if(!!s.save_selected && this.data.ui) { this.save_selected(); $.cookie(s.save_selected, this.data.ui.to_select.join(","), s.cookie_options); } break; } } } }); // include cookies by default // $.jstree.defaults.plugins.push("cookies"); })(jQuery); //*/ /* * jsTree sort plugin * Sorts items alphabetically (or using any other function) */ (function ($) { $.jstree.plugin("sort", { __init : function () { this.get_container() .bind("load_node.jstree", $.proxy(function (e, data) { var obj = this._get_node(data.rslt.obj); obj = obj === -1 ? this.get_container().children("ul") : obj.children("ul"); this.sort(obj); }, this)) .bind("rename_node.jstree create_node.jstree create.jstree", $.proxy(function (e, data) { this.sort(data.rslt.obj.parent()); }, this)) .bind("move_node.jstree", $.proxy(function (e, data) { var m = data.rslt.np == -1 ? this.get_container() : data.rslt.np; this.sort(m.children("ul")); }, this)); }, defaults : function (a, b) { return this.get_text(a) > this.get_text(b) ? 1 : -1; }, _fn : { sort : function (obj) { var s = this._get_settings().sort, t = this; obj.append($.makeArray(obj.children("li")).sort($.proxy(s, t))); obj.find("> li > ul").each(function() { t.sort($(this)); }); this.clean_node(obj); } } }); })(jQuery); //*/ /* * jsTree DND plugin * Drag and drop plugin for moving/copying nodes */ (function ($) { var o = false, r = false, m = false, ml = false, sli = false, sti = false, dir1 = false, dir2 = false, last_pos = false; $.vakata.dnd = { is_down : false, is_drag : false, helper : false, scroll_spd : 10, init_x : 0, init_y : 0, threshold : 5, helper_left : 5, helper_top : 10, user_data : {}, drag_start : function (e, data, html) { if($.vakata.dnd.is_drag) { $.vakata.drag_stop({}); } try { e.currentTarget.unselectable = "on"; e.currentTarget.onselectstart = function() { return false; }; if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; } } catch(err) { } $.vakata.dnd.init_x = e.pageX; $.vakata.dnd.init_y = e.pageY; $.vakata.dnd.user_data = data; $.vakata.dnd.is_down = true; $.vakata.dnd.helper = $("<div id='vakata-dragged' />").html(html); //.fadeTo(10,0.25); $(document).bind("mousemove", $.vakata.dnd.drag); $(document).bind("mouseup", $.vakata.dnd.drag_stop); return false; }, drag : function (e) { if(!$.vakata.dnd.is_down) { return; } if(!$.vakata.dnd.is_drag) { if(Math.abs(e.pageX - $.vakata.dnd.init_x) > 5 || Math.abs(e.pageY - $.vakata.dnd.init_y) > 5) { $.vakata.dnd.helper.appendTo("body"); $.vakata.dnd.is_drag = true; $(document).triggerHandler("drag_start.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); } else { return; } } // maybe use a scrolling parent element instead of document? if(e.type === "mousemove") { // thought of adding scroll in order to move the helper, but mouse poisition is n/a var d = $(document), t = d.scrollTop(), l = d.scrollLeft(); if(e.pageY - t < 20) { if(sti && dir1 === "down") { clearInterval(sti); sti = false; } if(!sti) { dir1 = "up"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() - $.vakata.dnd.scroll_spd); }, 150); } } else { if(sti && dir1 === "up") { clearInterval(sti); sti = false; } } if($(window).height() - (e.pageY - t) < 20) { if(sti && dir1 === "up") { clearInterval(sti); sti = false; } if(!sti) { dir1 = "down"; sti = setInterval(function () { $(document).scrollTop($(document).scrollTop() + $.vakata.dnd.scroll_spd); }, 150); } } else { if(sti && dir1 === "down") { clearInterval(sti); sti = false; } } if(e.pageX - l < 20) { if(sli && dir2 === "right") { clearInterval(sli); sli = false; } if(!sli) { dir2 = "left"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() - $.vakata.dnd.scroll_spd); }, 150); } } else { if(sli && dir2 === "left") { clearInterval(sli); sli = false; } } if($(window).width() - (e.pageX - l) < 20) { if(sli && dir2 === "left") { clearInterval(sli); sli = false; } if(!sli) { dir2 = "right"; sli = setInterval(function () { $(document).scrollLeft($(document).scrollLeft() + $.vakata.dnd.scroll_spd); }, 150); } } else { if(sli && dir2 === "right") { clearInterval(sli); sli = false; } } } $.vakata.dnd.helper.css({ left : (e.pageX + $.vakata.dnd.helper_left) + "px", top : (e.pageY + $.vakata.dnd.helper_top) + "px" }); $(document).triggerHandler("drag.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); }, drag_stop : function (e) { if(sli) { clearInterval(sli); } if(sti) { clearInterval(sti); } $(document).unbind("mousemove", $.vakata.dnd.drag); $(document).unbind("mouseup", $.vakata.dnd.drag_stop); $(document).triggerHandler("drag_stop.vakata", { "event" : e, "data" : $.vakata.dnd.user_data }); $.vakata.dnd.helper.remove(); $.vakata.dnd.init_x = 0; $.vakata.dnd.init_y = 0; $.vakata.dnd.user_data = {}; $.vakata.dnd.is_down = false; $.vakata.dnd.is_drag = false; } }; $(function() { var css_string = '#vakata-dragged { display:block; margin:0 0 0 0; padding:4px 4px 4px 24px; position:absolute; top:-2000px; line-height:16px; z-index:10000; } '; $.vakata.css.add_sheet({ str : css_string, title : "vakata" }); }); $.jstree.plugin("dnd", { __init : function () { this.data.dnd = { active : false, after : false, inside : false, before : false, off : false, prepared : false, w : 0, to1 : false, to2 : false, cof : false, cw : false, ch : false, i1 : false, i2 : false, mto : false }; this.get_container() .bind("mouseenter.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(this.data.themes) { m.attr("class", "jstree-" + this.data.themes.theme); if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } //if($(e.currentTarget).find("> ul > li").length === 0) { if(e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree var tr = $.jstree._reference(e.target), dc; if(tr.data.dnd.foreign) { dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); if(dc === true || dc.inside === true || dc.before === true || dc.after === true) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } } else { tr.prepare_move(o, tr.get_container(), "last"); if(tr.check_move()) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } } } } }, this)) .bind("mouseup.jstree", $.proxy(function (e) { //if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && $(e.currentTarget).find("> ul > li").length === 0) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && e.currentTarget === e.target && $.vakata.dnd.user_data.obj && $($.vakata.dnd.user_data.obj).length && $($.vakata.dnd.user_data.obj).parents(".jstree:eq(0)")[0] !== e.target) { // node should not be from the same tree var tr = $.jstree._reference(e.currentTarget), dc; if(tr.data.dnd.foreign) { dc = tr._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); if(dc === true || dc.inside === true || dc.before === true || dc.after === true) { tr._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : tr.get_container(), is_root : true }); } } else { tr.move_node(o, tr.get_container(), "last", e[tr._get_settings().dnd.copy_modifier + "Key"]); } } }, this)) .bind("mouseleave.jstree", $.proxy(function (e) { if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") { return false; } if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if($.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } } }, this)) .bind("mousemove.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { var cnt = this.get_container()[0]; // Horizontal scroll if(e.pageX + 24 > this.data.dnd.cof.left + this.data.dnd.cw) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft += $.vakata.dnd.scroll_spd; }, cnt), 100); } else if(e.pageX - 24 < this.data.dnd.cof.left) { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } this.data.dnd.i1 = setInterval($.proxy(function () { this.scrollLeft -= $.vakata.dnd.scroll_spd; }, cnt), 100); } else { if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } } // Vertical scroll if(e.pageY + 24 > this.data.dnd.cof.top + this.data.dnd.ch) { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop += $.vakata.dnd.scroll_spd; }, cnt), 100); } else if(e.pageY - 24 < this.data.dnd.cof.top) { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.i2 = setInterval($.proxy(function () { this.scrollTop -= $.vakata.dnd.scroll_spd; }, cnt), 100); } else { if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } } } }, this)) .bind("scroll.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && m && ml) { m.hide(); ml.hide(); } }, this)) .delegate("a", "mousedown.jstree", $.proxy(function (e) { if(e.which === 1) { this.start_drag(e.currentTarget, e); return false; } }, this)) .delegate("a", "mouseenter.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { this.dnd_enter(e.currentTarget); } }, this)) .delegate("a", "mousemove.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(!r || !r.length || r.children("a")[0] !== e.currentTarget) { this.dnd_enter(e.currentTarget); } if(typeof this.data.dnd.off.top === "undefined") { this.data.dnd.off = $(e.target).offset(); } this.data.dnd.w = (e.pageY - (this.data.dnd.off.top || 0)) % this.data.core.li_height; if(this.data.dnd.w < 0) { this.data.dnd.w += this.data.core.li_height; } this.dnd_show(); } }, this)) .delegate("a", "mouseleave.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { if(e.relatedTarget && e.relatedTarget.id && e.relatedTarget.id === "jstree-marker-line") { return false; } if(m) { m.hide(); } if(ml) { ml.hide(); } /* var ec = $(e.currentTarget).closest("li"), er = $(e.relatedTarget).closest("li"); if(er[0] !== ec.prev()[0] && er[0] !== ec.next()[0]) { if(m) { m.hide(); } if(ml) { ml.hide(); } } */ this.data.dnd.mto = setTimeout( (function (t) { return function () { t.dnd_leave(e); }; })(this), 0); } }, this)) .delegate("a", "mouseup.jstree", $.proxy(function (e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree) { this.dnd_finish(e); } }, this)); $(document) .bind("drag_stop.vakata", $.proxy(function () { if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if(this.data.dnd.i1) { clearInterval(this.data.dnd.i1); } if(this.data.dnd.i2) { clearInterval(this.data.dnd.i2); } this.data.dnd.after = false; this.data.dnd.before = false; this.data.dnd.inside = false; this.data.dnd.off = false; this.data.dnd.prepared = false; this.data.dnd.w = false; this.data.dnd.to1 = false; this.data.dnd.to2 = false; this.data.dnd.i1 = false; this.data.dnd.i2 = false; this.data.dnd.active = false; this.data.dnd.foreign = false; if(m) { m.css({ "top" : "-2000px" }); } if(ml) { ml.css({ "top" : "-2000px" }); } }, this)) .bind("drag_start.vakata", $.proxy(function (e, data) { if(data.data.jstree) { var et = $(data.event.target); if(et.closest(".jstree").hasClass("jstree-" + this.get_index())) { this.dnd_enter(et); } } }, this)); /* .bind("keydown.jstree-" + this.get_index() + " keyup.jstree-" + this.get_index(), $.proxy(function(e) { if($.vakata.dnd.is_drag && $.vakata.dnd.user_data.jstree && !this.data.dnd.foreign) { var h = $.vakata.dnd.helper.children("ins"); if(e[this._get_settings().dnd.copy_modifier + "Key"] && h.hasClass("jstree-ok")) { h.parent().html(h.parent().html().replace(/ \(Copy\)$/, "") + " (Copy)"); } else { h.parent().html(h.parent().html().replace(/ \(Copy\)$/, "")); } } }, this)); */ var s = this._get_settings().dnd; if(s.drag_target) { $(document) .delegate(s.drag_target, "mousedown.jstree-" + this.get_index(), $.proxy(function (e) { o = e.target; $.vakata.dnd.drag_start(e, { jstree : true, obj : e.target }, "<ins class='jstree-icon'></ins>" + $(e.target).text() ); if(this.data.themes) { if(m) { m.attr("class", "jstree-" + this.data.themes.theme); } if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); var cnt = this.get_container(); this.data.dnd.cof = cnt.offset(); this.data.dnd.cw = parseInt(cnt.width(),10); this.data.dnd.ch = parseInt(cnt.height(),10); this.data.dnd.foreign = true; e.preventDefault(); }, this)); } if(s.drop_target) { $(document) .delegate(s.drop_target, "mouseenter.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active && this._get_settings().dnd.drop_check.call(this, { "o" : o, "r" : $(e.target), "e" : e })) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); } }, this)) .delegate(s.drop_target, "mouseleave.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } }, this)) .delegate(s.drop_target, "mouseup.jstree-" + this.get_index(), $.proxy(function (e) { if(this.data.dnd.active && $.vakata.dnd.helper.children("ins").hasClass("jstree-ok")) { this._get_settings().dnd.drop_finish.call(this, { "o" : o, "r" : $(e.target), "e" : e }); } }, this)); } }, defaults : { copy_modifier : "ctrl", check_timeout : 100, open_timeout : 500, drop_target : ".jstree-drop", drop_check : function (data) { return true; }, drop_finish : $.noop, drag_target : ".jstree-draggable", drag_finish : $.noop, drag_check : function (data) { return { after : false, before : false, inside : true }; } }, _fn : { dnd_prepare : function () { if(!r || !r.length) { return; } this.data.dnd.off = r.offset(); if(this._get_settings().core.rtl) { this.data.dnd.off.right = this.data.dnd.off.left + r.width(); } if(this.data.dnd.foreign) { var a = this._get_settings().dnd.drag_check.call(this, { "o" : o, "r" : r }); this.data.dnd.after = a.after; this.data.dnd.before = a.before; this.data.dnd.inside = a.inside; this.data.dnd.prepared = true; return this.dnd_show(); } this.prepare_move(o, r, "before"); this.data.dnd.before = this.check_move(); this.prepare_move(o, r, "after"); this.data.dnd.after = this.check_move(); if(this._is_loaded(r)) { this.prepare_move(o, r, "inside"); this.data.dnd.inside = this.check_move(); } else { this.data.dnd.inside = false; } this.data.dnd.prepared = true; return this.dnd_show(); }, dnd_show : function () { if(!this.data.dnd.prepared) { return; } var o = ["before","inside","after"], r = false, rtl = this._get_settings().core.rtl, pos; if(this.data.dnd.w < this.data.core.li_height/3) { o = ["before","inside","after"]; } else if(this.data.dnd.w <= this.data.core.li_height*2/3) { o = this.data.dnd.w < this.data.core.li_height/2 ? ["inside","before","after"] : ["inside","after","before"]; } else { o = ["after","inside","before"]; } $.each(o, $.proxy(function (i, val) { if(this.data.dnd[val]) { $.vakata.dnd.helper.children("ins").attr("class","jstree-ok"); r = val; return false; } }, this)); if(r === false) { $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); } pos = rtl ? (this.data.dnd.off.right - 18) : (this.data.dnd.off.left + 10); switch(r) { case "before": m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top - 6) + "px" }).show(); if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top - 1) + "px" }).show(); } break; case "after": m.css({ "left" : pos + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 6) + "px" }).show(); if(ml) { ml.css({ "left" : (pos + 8) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height - 1) + "px" }).show(); } break; case "inside": m.css({ "left" : pos + ( rtl ? -4 : 4) + "px", "top" : (this.data.dnd.off.top + this.data.core.li_height/2 - 5) + "px" }).show(); if(ml) { ml.hide(); } break; default: m.hide(); if(ml) { ml.hide(); } break; } last_pos = r; return r; }, dnd_open : function () { this.data.dnd.to2 = false; this.open_node(r, $.proxy(this.dnd_prepare,this), true); }, dnd_finish : function (e) { if(this.data.dnd.foreign) { if(this.data.dnd.after || this.data.dnd.before || this.data.dnd.inside) { this._get_settings().dnd.drag_finish.call(this, { "o" : o, "r" : r, "p" : last_pos }); } } else { this.dnd_prepare(); this.move_node(o, r, last_pos, e[this._get_settings().dnd.copy_modifier + "Key"]); } o = false; r = false; m.hide(); if(ml) { ml.hide(); } }, dnd_enter : function (obj) { if(this.data.dnd.mto) { clearTimeout(this.data.dnd.mto); this.data.dnd.mto = false; } var s = this._get_settings().dnd; this.data.dnd.prepared = false; r = this._get_node(obj); if(s.check_timeout) { // do the calculations after a minimal timeout (users tend to drag quickly to the desired location) if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); } this.data.dnd.to1 = setTimeout($.proxy(this.dnd_prepare, this), s.check_timeout); } else { this.dnd_prepare(); } if(s.open_timeout) { if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); } if(r && r.length && r.hasClass("jstree-closed")) { // if the node is closed - open it, then recalculate this.data.dnd.to2 = setTimeout($.proxy(this.dnd_open, this), s.open_timeout); } } else { if(r && r.length && r.hasClass("jstree-closed")) { this.dnd_open(); } } }, dnd_leave : function (e) { this.data.dnd.after = false; this.data.dnd.before = false; this.data.dnd.inside = false; $.vakata.dnd.helper.children("ins").attr("class","jstree-invalid"); m.hide(); if(ml) { ml.hide(); } if(r && r[0] === e.target.parentNode) { if(this.data.dnd.to1) { clearTimeout(this.data.dnd.to1); this.data.dnd.to1 = false; } if(this.data.dnd.to2) { clearTimeout(this.data.dnd.to2); this.data.dnd.to2 = false; } } }, start_drag : function (obj, e) { o = this._get_node(obj); if(this.data.ui && this.is_selected(o)) { o = this._get_node(null, true); } var dt = o.length > 1 ? this._get_string("multiple_selection") : this.get_text(o), cnt = this.get_container(); if(!this._get_settings().core.html_titles) { dt = dt.replace(/</ig,"<").replace(/>/ig,">"); } $.vakata.dnd.drag_start(e, { jstree : true, obj : o }, "<ins class='jstree-icon'></ins>" + dt ); if(this.data.themes) { if(m) { m.attr("class", "jstree-" + this.data.themes.theme); } if(ml) { ml.attr("class", "jstree-" + this.data.themes.theme); } $.vakata.dnd.helper.attr("class", "jstree-dnd-helper jstree-" + this.data.themes.theme); } this.data.dnd.cof = cnt.offset(); this.data.dnd.cw = parseInt(cnt.width(),10); this.data.dnd.ch = parseInt(cnt.height(),10); this.data.dnd.active = true; } } }); $(function() { var css_string = '' + '#vakata-dragged ins { display:block; text-decoration:none; width:16px; height:16px; margin:0 0 0 0; padding:0; position:absolute; top:4px; left:4px; ' + ' -moz-border-radius:4px; border-radius:4px; -webkit-border-radius:4px; ' + '} ' + '#vakata-dragged .jstree-ok { background:green; } ' + '#vakata-dragged .jstree-invalid { background:red; } ' + '#jstree-marker { padding:0; margin:0; font-size:12px; overflow:hidden; height:12px; width:8px; position:absolute; top:-30px; z-index:10001; background-repeat:no-repeat; display:none; background-color:transparent; text-shadow:1px 1px 1px white; color:black; line-height:10px; } ' + '#jstree-marker-line { padding:0; margin:0; line-height:0%; font-size:1px; overflow:hidden; height:1px; width:100px; position:absolute; top:-30px; z-index:10000; background-repeat:no-repeat; display:none; background-color:#456c43; ' + ' cursor:pointer; border:1px solid #eeeeee; border-left:0; -moz-box-shadow: 0px 0px 2px #666; -webkit-box-shadow: 0px 0px 2px #666; box-shadow: 0px 0px 2px #666; ' + ' -moz-border-radius:1px; border-radius:1px; -webkit-border-radius:1px; ' + '}' + ''; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); m = $("<div />").attr({ id : "jstree-marker" }).hide().html("»") .bind("mouseleave mouseenter", function (e) { m.hide(); ml.hide(); e.preventDefault(); e.stopImmediatePropagation(); return false; }) .appendTo("body"); ml = $("<div />").attr({ id : "jstree-marker-line" }).hide() .bind("mouseup", function (e) { if(r && r.length) { r.children("a").trigger(e); e.preventDefault(); e.stopImmediatePropagation(); return false; } }) .bind("mouseleave", function (e) { var rt = $(e.relatedTarget); if(rt.is(".jstree") || rt.closest(".jstree").length === 0) { if(r && r.length) { r.children("a").trigger(e); m.hide(); ml.hide(); e.preventDefault(); e.stopImmediatePropagation(); return false; } } }) .appendTo("body"); $(document).bind("drag_start.vakata", function (e, data) { if(data.data.jstree) { m.show(); if(ml) { ml.show(); } } }); $(document).bind("drag_stop.vakata", function (e, data) { if(data.data.jstree) { m.hide(); if(ml) { ml.hide(); } } }); }); })(jQuery); //*/ /* * jsTree checkbox plugin * Inserts checkboxes in front of every node * Depends on the ui plugin * DOES NOT WORK NICELY WITH MULTITREE DRAG'N'DROP */ (function ($) { $.jstree.plugin("checkbox", { __init : function () { this.data.checkbox.noui = this._get_settings().checkbox.override_ui; if(this.data.ui && this.data.checkbox.noui) { this.select_node = this.deselect_node = this.deselect_all = $.noop; this.get_selected = this.get_checked; } this.get_container() .bind("open_node.jstree create_node.jstree clean_node.jstree refresh.jstree", $.proxy(function (e, data) { this._prepare_checkboxes(data.rslt.obj); }, this)) .bind("loaded.jstree", $.proxy(function (e) { this._prepare_checkboxes(); }, this)) .delegate( (this.data.ui && this.data.checkbox.noui ? "a" : "ins.jstree-checkbox") , "click.jstree", $.proxy(function (e) { e.preventDefault(); if(this._get_node(e.target).hasClass("jstree-checked")) { this.uncheck_node(e.target); } else { this.check_node(e.target); } if(this.data.ui && this.data.checkbox.noui) { this.save_selected(); if(this.data.cookies) { this.save_cookie("select_node"); } } else { e.stopImmediatePropagation(); return false; } }, this)); }, defaults : { override_ui : false, two_state : false, real_checkboxes : false, checked_parent_open : true, real_checkboxes_names : function (n) { return [ ("check_" + (n[0].id || Math.ceil(Math.random() * 10000))) , 1]; } }, __destroy : function () { this.get_container() .find("input.jstree-real-checkbox").removeClass("jstree-real-checkbox").end() .find("ins.jstree-checkbox").remove(); }, _fn : { _checkbox_notify : function (n, data) { if(data.checked) { this.check_node(n, false); } }, _prepare_checkboxes : function (obj) { obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj); if(obj === false) { return; } // added for removing root nodes var c, _this = this, t, ts = this._get_settings().checkbox.two_state, rc = this._get_settings().checkbox.real_checkboxes, rcn = this._get_settings().checkbox.real_checkboxes_names; obj.each(function () { t = $(this); c = t.is("li") && (t.hasClass("jstree-checked") || (rc && t.children(":checked").length)) ? "jstree-checked" : "jstree-unchecked"; t.find("li").andSelf().each(function () { var $t = $(this), nm; $t.children("a" + (_this.data.languages ? "" : ":eq(0)") ).not(":has(.jstree-checkbox)").prepend("<ins class='jstree-checkbox'> </ins>").parent().not(".jstree-checked, .jstree-unchecked").addClass( ts ? "jstree-unchecked" : c ); if(rc) { if(!$t.children(":checkbox").length) { nm = rcn.call(_this, $t); $t.prepend("<input type='checkbox' class='jstree-real-checkbox' id='" + nm[0] + "' name='" + nm[0] + "' value='" + nm[1] + "' />"); } else { $t.children(":checkbox").addClass("jstree-real-checkbox"); } } if(!ts) { if(c === "jstree-checked" || $t.hasClass("jstree-checked") || $t.children(':checked').length) { $t.find("li").andSelf().addClass("jstree-checked").children(":checkbox").prop("checked", true); } } else { if($t.hasClass("jstree-checked") || $t.children(':checked').length) { $t.addClass("jstree-checked").children(":checkbox").prop("checked", true); } } }); }); if(!ts) { obj.find(".jstree-checked").parent().parent().each(function () { _this._repair_state(this); }); } }, change_state : function (obj, state) { obj = this._get_node(obj); var coll = false, rc = this._get_settings().checkbox.real_checkboxes; if(!obj || obj === -1) { return false; } state = (state === false || state === true) ? state : obj.hasClass("jstree-checked"); if(this._get_settings().checkbox.two_state) { if(state) { obj.removeClass("jstree-checked").addClass("jstree-unchecked"); if(rc) { obj.children(":checkbox").prop("checked", false); } } else { obj.removeClass("jstree-unchecked").addClass("jstree-checked"); if(rc) { obj.children(":checkbox").prop("checked", true); } } } else { if(state) { coll = obj.find("li").andSelf(); if(!coll.filter(".jstree-checked, .jstree-undetermined").length) { return false; } coll.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked"); if(rc) { coll.children(":checkbox").prop("checked", false); } } else { coll = obj.find("li").andSelf(); if(!coll.filter(".jstree-unchecked, .jstree-undetermined").length) { return false; } coll.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked"); if(rc) { coll.children(":checkbox").prop("checked", true); } if(this.data.ui) { this.data.ui.last_selected = obj; } this.data.checkbox.last_selected = obj; } obj.parentsUntil(".jstree", "li").each(function () { var $this = $(this); if(state) { if($this.children("ul").children("li.jstree-checked, li.jstree-undetermined").length) { $this.parentsUntil(".jstree", "li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { $this.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } return false; } else { $this.removeClass("jstree-checked jstree-undetermined").addClass("jstree-unchecked"); if(rc) { $this.children(":checkbox").prop("checked", false); } } } else { if($this.children("ul").children("li.jstree-unchecked, li.jstree-undetermined").length) { $this.parentsUntil(".jstree", "li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { $this.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } return false; } else { $this.removeClass("jstree-unchecked jstree-undetermined").addClass("jstree-checked"); if(rc) { $this.children(":checkbox").prop("checked", true); } } } }); } if(this.data.ui && this.data.checkbox.noui) { this.data.ui.selected = this.get_checked(); } this.__callback(obj); return true; }, check_node : function (obj) { if(this.change_state(obj, false)) { obj = this._get_node(obj); if(this._get_settings().checkbox.checked_parent_open) { var t = this; obj.parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); } this.__callback({ "obj" : obj }); } }, uncheck_node : function (obj) { if(this.change_state(obj, true)) { this.__callback({ "obj" : this._get_node(obj) }); } }, check_all : function () { var _this = this, coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li"); coll.each(function () { _this.change_state(this, false); }); this.__callback(); }, uncheck_all : function () { var _this = this, coll = this._get_settings().checkbox.two_state ? this.get_container_ul().find("li") : this.get_container_ul().children("li"); coll.each(function () { _this.change_state(this, true); }); this.__callback(); }, is_checked : function(obj) { obj = this._get_node(obj); return obj.length ? obj.is(".jstree-checked") : false; }, get_checked : function (obj, get_all) { obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj); return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-checked") : obj.find("> ul > .jstree-checked, .jstree-undetermined > ul > .jstree-checked"); }, get_unchecked : function (obj, get_all) { obj = !obj || obj === -1 ? this.get_container() : this._get_node(obj); return get_all || this._get_settings().checkbox.two_state ? obj.find(".jstree-unchecked") : obj.find("> ul > .jstree-unchecked, .jstree-undetermined > ul > .jstree-unchecked"); }, show_checkboxes : function () { this.get_container().children("ul").removeClass("jstree-no-checkboxes"); }, hide_checkboxes : function () { this.get_container().children("ul").addClass("jstree-no-checkboxes"); }, _repair_state : function (obj) { obj = this._get_node(obj); if(!obj.length) { return; } if(this._get_settings().checkbox.two_state) { obj.find('li').andSelf().not('.jstree-checked').removeClass('jstree-undetermined').addClass('jstree-unchecked').children(':checkbox').prop('checked', true); return; } var rc = this._get_settings().checkbox.real_checkboxes, a = obj.find("> ul > .jstree-checked").length, b = obj.find("> ul > .jstree-undetermined").length, c = obj.find("> ul > li").length; if(c === 0) { if(obj.hasClass("jstree-undetermined")) { this.change_state(obj, false); } } else if(a === 0 && b === 0) { this.change_state(obj, true); } else if(a === c) { this.change_state(obj, false); } else { obj.parentsUntil(".jstree","li").andSelf().removeClass("jstree-checked jstree-unchecked").addClass("jstree-undetermined"); if(rc) { obj.parentsUntil(".jstree", "li").andSelf().children(":checkbox").prop("checked", false); } } }, reselect : function () { if(this.data.ui && this.data.checkbox.noui) { var _this = this, s = this.data.ui.to_select; s = $.map($.makeArray(s), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); this.deselect_all(); $.each(s, function (i, val) { _this.check_node(val); }); this.__callback(); } else { this.__call_old(); } }, save_loaded : function () { var _this = this; this.data.core.to_load = []; this.get_container_ul().find("li.jstree-closed.jstree-undetermined").each(function () { if(this.id) { _this.data.core.to_load.push("#" + this.id); } }); } } }); $(function() { var css_string = '.jstree .jstree-real-checkbox { display:none; } '; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree XML plugin * The XML data store. Datastores are build by overriding the `load_node` and `_is_loaded` functions. */ (function ($) { $.vakata.xslt = function (xml, xsl, callback) { var rs = "", xm, xs, processor, support; // TODO: IE9 no XSLTProcessor, no document.recalc if(document.recalc) { xm = document.createElement('xml'); xs = document.createElement('xml'); xm.innerHTML = xml; xs.innerHTML = xsl; $("body").append(xm).append(xs); setTimeout( (function (xm, xs, callback) { return function () { callback.call(null, xm.transformNode(xs.XMLDocument)); setTimeout( (function (xm, xs) { return function () { $(xm).remove(); $(xs).remove(); }; })(xm, xs), 200); }; })(xm, xs, callback), 100); return true; } if(typeof window.DOMParser !== "undefined" && typeof window.XMLHttpRequest !== "undefined" && typeof window.XSLTProcessor === "undefined") { xml = new DOMParser().parseFromString(xml, "text/xml"); xsl = new DOMParser().parseFromString(xsl, "text/xml"); // alert(xml.transformNode()); // callback.call(null, new XMLSerializer().serializeToString(rs)); } if(typeof window.DOMParser !== "undefined" && typeof window.XMLHttpRequest !== "undefined" && typeof window.XSLTProcessor !== "undefined") { processor = new XSLTProcessor(); support = $.isFunction(processor.transformDocument) ? (typeof window.XMLSerializer !== "undefined") : true; if(!support) { return false; } xml = new DOMParser().parseFromString(xml, "text/xml"); xsl = new DOMParser().parseFromString(xsl, "text/xml"); if($.isFunction(processor.transformDocument)) { rs = document.implementation.createDocument("", "", null); processor.transformDocument(xml, xsl, rs, null); callback.call(null, new XMLSerializer().serializeToString(rs)); return true; } else { processor.importStylesheet(xsl); rs = processor.transformToFragment(xml, document); callback.call(null, $("<div />").append(rs).html()); return true; } } return false; }; var xsl = { 'nest' : '<' + '?xml version="1.0" encoding="utf-8" ?>' + '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' + '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/html" />' + '<xsl:template match="/">' + ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="/root" />' + ' </xsl:call-template>' + '</xsl:template>' + '<xsl:template name="nodes">' + ' <xsl:param name="node" />' + ' <ul>' + ' <xsl:for-each select="$node/item">' + ' <xsl:variable name="children" select="count(./item) > 0" />' + ' <li>' + ' <xsl:attribute name="class">' + ' <xsl:if test="position() = last()">jstree-last </xsl:if>' + ' <xsl:choose>' + ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' + ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' + ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' + ' </xsl:choose>' + ' <xsl:value-of select="@class" />' + ' </xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'class\' and name() != \'state\' and name() != \'hasChildren\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins class="jstree-icon"><xsl:text> </xsl:text></ins>' + ' <xsl:for-each select="content/name">' + ' <a>' + ' <xsl:attribute name="href">' + ' <xsl:choose>' + ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' + ' <xsl:otherwise>#</xsl:otherwise>' + ' </xsl:choose>' + ' </xsl:attribute>' + ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' + ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins>' + ' <xsl:attribute name="class">jstree-icon ' + ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' + ' </xsl:attribute>' + ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' + ' <xsl:text> </xsl:text>' + ' </ins>' + ' <xsl:copy-of select="./child::node()" />' + ' </a>' + ' </xsl:for-each>' + ' <xsl:if test="$children or @hasChildren"><xsl:call-template name="nodes"><xsl:with-param name="node" select="current()" /></xsl:call-template></xsl:if>' + ' </li>' + ' </xsl:for-each>' + ' </ul>' + '</xsl:template>' + '</xsl:stylesheet>', 'flat' : '<' + '?xml version="1.0" encoding="utf-8" ?>' + '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >' + '<xsl:output method="html" encoding="utf-8" omit-xml-declaration="yes" standalone="no" indent="no" media-type="text/xml" />' + '<xsl:template match="/">' + ' <ul>' + ' <xsl:for-each select="//item[not(@parent_id) or @parent_id=0 or not(@parent_id = //item/@id)]">' + /* the last `or` may be removed */ ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="." />' + ' <xsl:with-param name="is_last" select="number(position() = last())" />' + ' </xsl:call-template>' + ' </xsl:for-each>' + ' </ul>' + '</xsl:template>' + '<xsl:template name="nodes">' + ' <xsl:param name="node" />' + ' <xsl:param name="is_last" />' + ' <xsl:variable name="children" select="count(//item[@parent_id=$node/attribute::id]) > 0" />' + ' <li>' + ' <xsl:attribute name="class">' + ' <xsl:if test="$is_last = true()">jstree-last </xsl:if>' + ' <xsl:choose>' + ' <xsl:when test="@state = \'open\'">jstree-open </xsl:when>' + ' <xsl:when test="$children or @hasChildren or @state = \'closed\'">jstree-closed </xsl:when>' + ' <xsl:otherwise>jstree-leaf </xsl:otherwise>' + ' </xsl:choose>' + ' <xsl:value-of select="@class" />' + ' </xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'parent_id\' and name() != \'hasChildren\' and name() != \'class\' and name() != \'state\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins class="jstree-icon"><xsl:text> </xsl:text></ins>' + ' <xsl:for-each select="content/name">' + ' <a>' + ' <xsl:attribute name="href">' + ' <xsl:choose>' + ' <xsl:when test="@href"><xsl:value-of select="@href" /></xsl:when>' + ' <xsl:otherwise>#</xsl:otherwise>' + ' </xsl:choose>' + ' </xsl:attribute>' + ' <xsl:attribute name="class"><xsl:value-of select="@lang" /> <xsl:value-of select="@class" /></xsl:attribute>' + ' <xsl:attribute name="style"><xsl:value-of select="@style" /></xsl:attribute>' + ' <xsl:for-each select="@*">' + ' <xsl:if test="name() != \'style\' and name() != \'class\' and name() != \'href\'">' + ' <xsl:attribute name="{name()}"><xsl:value-of select="." /></xsl:attribute>' + ' </xsl:if>' + ' </xsl:for-each>' + ' <ins>' + ' <xsl:attribute name="class">jstree-icon ' + ' <xsl:if test="string-length(attribute::icon) > 0 and not(contains(@icon,\'/\'))"><xsl:value-of select="@icon" /></xsl:if>' + ' </xsl:attribute>' + ' <xsl:if test="string-length(attribute::icon) > 0 and contains(@icon,\'/\')"><xsl:attribute name="style">background:url(<xsl:value-of select="@icon" />) center center no-repeat;</xsl:attribute></xsl:if>' + ' <xsl:text> </xsl:text>' + ' </ins>' + ' <xsl:copy-of select="./child::node()" />' + ' </a>' + ' </xsl:for-each>' + ' <xsl:if test="$children">' + ' <ul>' + ' <xsl:for-each select="//item[@parent_id=$node/attribute::id]">' + ' <xsl:call-template name="nodes">' + ' <xsl:with-param name="node" select="." />' + ' <xsl:with-param name="is_last" select="number(position() = last())" />' + ' </xsl:call-template>' + ' </xsl:for-each>' + ' </ul>' + ' </xsl:if>' + ' </li>' + '</xsl:template>' + '</xsl:stylesheet>' }, escape_xml = function(string) { return string .toString() .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; $.jstree.plugin("xml_data", { defaults : { data : false, ajax : false, xsl : "flat", clean_node : false, correct_state : true, get_skip_empty : false, get_include_preamble : true }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_xml(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { var s = this._get_settings().xml_data; obj = this._get_node(obj); return obj == -1 || !obj || (!s.ajax && !$.isFunction(s.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0; }, load_node_xml : function (obj, s_call, e_call) { var s = this.get_settings().xml_data, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case (!s.data && !s.ajax): throw "Neither data nor ajax settings supplied."; case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { this.parse_xml(d, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); } if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } } }, this)); }, this)); break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { this.parse_xml(s.data, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); this.get_container().children("ul").empty().append(d.children()); if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } }, this)); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): error_func = function (x, t, e) { var ef = this.get_settings().xml_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj !== -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { d = x.responseText; var sf = this.get_settings().xml_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) { return error_func.call(this, x, t, ""); } this.parse_xml(d, $.proxy(function (d) { if(d) { d = d.replace(/ ?xmlns="[^"]*"/ig, ""); if(d.length > 10) { d = $(d); if(obj === -1 || !obj) { this.get_container().children("ul").empty().append(d.children()); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d); obj.removeData("jstree_is_loading"); } if(s.clean_node) { this.clean_node(obj); } if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } } }, this)); }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "xml"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } }, parse_xml : function (xml, callback) { var s = this._get_settings().xml_data; $.vakata.xslt(xml, xsl[s.xsl], callback); }, get_xml : function (tp, obj, li_attr, a_attr, is_callback) { var result = "", s = this._get_settings(), _this = this, tmp1, tmp2, li, a, lang; if(!tp) { tp = "flat"; } if(!is_callback) { is_callback = 0; } obj = this._get_node(obj); if(!obj || obj === -1) { obj = this.get_container().find("> ul > li"); } li_attr = $.isArray(li_attr) ? li_attr : [ "id", "class" ]; if(!is_callback && this.data.types && $.inArray(s.types.type_attr, li_attr) === -1) { li_attr.push(s.types.type_attr); } a_attr = $.isArray(a_attr) ? a_attr : [ ]; if(!is_callback) { if(s.xml_data.get_include_preamble) { result += '<' + '?xml version="1.0" encoding="UTF-8"?' + '>'; } result += "<root>"; } obj.each(function () { result += "<item"; li = $(this); $.each(li_attr, function (i, v) { var t = li.attr(v); if(!s.xml_data.get_skip_empty || typeof t !== "undefined") { result += " " + v + "=\"" + escape_xml((" " + (t || "")).replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\""; } }); if(li.hasClass("jstree-open")) { result += " state=\"open\""; } if(li.hasClass("jstree-closed")) { result += " state=\"closed\""; } if(tp === "flat") { result += " parent_id=\"" + escape_xml(is_callback) + "\""; } result += ">"; result += "<content>"; a = li.children("a"); a.each(function () { tmp1 = $(this); lang = false; result += "<name"; if($.inArray("languages", s.plugins) !== -1) { $.each(s.languages, function (k, z) { if(tmp1.hasClass(z)) { result += " lang=\"" + escape_xml(z) + "\""; lang = z; return false; } }); } if(a_attr.length) { $.each(a_attr, function (k, z) { var t = tmp1.attr(z); if(!s.xml_data.get_skip_empty || typeof t !== "undefined") { result += " " + z + "=\"" + escape_xml((" " + t || "").replace(/ jstree[^ ]*/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + "\""; } }); } if(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/^\s+$/ig,"").length) { result += ' icon="' + escape_xml(tmp1.children("ins").get(0).className.replace(/jstree[^ ]*|$/ig,'').replace(/\s+$/ig," ").replace(/^ /,"").replace(/ $/,"")) + '"'; } if(tmp1.children("ins").get(0).style.backgroundImage.length) { result += ' icon="' + escape_xml(tmp1.children("ins").get(0).style.backgroundImage.replace("url(","").replace(")","").replace(/'/ig,"").replace(/"/ig,"")) + '"'; } result += ">"; result += "<![CDATA[" + _this.get_text(tmp1, lang) + "]]>"; result += "</name>"; }); result += "</content>"; tmp2 = li[0].id || true; li = li.find("> ul > li"); if(li.length) { tmp2 = _this.get_xml(tp, li, li_attr, a_attr, tmp2); } else { tmp2 = ""; } if(tp == "nest") { result += tmp2; } result += "</item>"; if(tp == "flat") { result += tmp2; } }); if(!is_callback) { result += "</root>"; } return result; } } }); })(jQuery); //*/ /* * jsTree search plugin * Enables both sync and async search on the tree * DOES NOT WORK WITH JSON PROGRESSIVE RENDER */ (function ($) { $.expr[':'].jstree_contains = function(a,i,m){ return (a.textContent || a.innerText || "").toLowerCase().indexOf(m[3].toLowerCase())>=0; }; $.expr[':'].jstree_title_contains = function(a,i,m) { return (a.getAttribute("title") || "").toLowerCase().indexOf(m[3].toLowerCase())>=0; }; $.jstree.plugin("search", { __init : function () { this.data.search.str = ""; this.data.search.result = $(); if(this._get_settings().search.show_only_matches) { this.get_container() .bind("search.jstree", function (e, data) { $(this).children("ul").find("li").hide().removeClass("jstree-last"); data.rslt.nodes.parentsUntil(".jstree").andSelf().show() .filter("ul").each(function () { $(this).children("li:visible").eq(-1).addClass("jstree-last"); }); }) .bind("clear_search.jstree", function () { $(this).children("ul").find("li").css("display","").end().end().jstree("clean_node", -1); }); } }, defaults : { ajax : false, search_method : "jstree_contains", // for case insensitive - jstree_contains show_only_matches : false }, _fn : { search : function (str, skip_async) { if($.trim(str) === "") { this.clear_search(); return; } var s = this.get_settings().search, t = this, error_func = function () { }, success_func = function () { }; this.data.search.str = str; if(!skip_async && s.ajax !== false && this.get_container_ul().find("li.jstree-closed:not(:has(ul)):eq(0)").length > 0) { this.search.supress_callback = true; error_func = function () { }; success_func = function (d, t, x) { var sf = this.get_settings().search.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } this.data.search.to_open = d; this._search_open(); }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, str); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, str); } if(!s.ajax.data) { s.ajax.data = { "search_string" : str }; } if(!s.ajax.dataType || /^json/.exec(s.ajax.dataType)) { s.ajax.dataType = "json"; } $.ajax(s.ajax); return; } if(this.data.search.result.length) { this.clear_search(); } this.data.search.result = this.get_container().find("a" + (this.data.languages ? "." + this.get_lang() : "" ) + ":" + (s.search_method) + "(" + this.data.search.str + ")"); this.data.search.result.addClass("jstree-search").parent().parents(".jstree-closed").each(function () { t.open_node(this, false, true); }); this.__callback({ nodes : this.data.search.result, str : str }); }, clear_search : function (str) { this.data.search.result.removeClass("jstree-search"); this.__callback(this.data.search.result); this.data.search.result = $(); }, _search_open : function (is_callback) { var _this = this, done = true, current = [], remaining = []; if(this.data.search.to_open.length) { $.each(this.data.search.to_open, function (i, val) { if(val == "#") { return true; } if($(val).length && $(val).is(".jstree-closed")) { current.push(val); } else { remaining.push(val); } }); if(current.length) { this.data.search.to_open = remaining; $.each(current, function (i, val) { _this.open_node(val, function () { _this._search_open(true); }); }); done = false; } } if(done) { this.search(this.data.search.str, true); } } } }); })(jQuery); //*/ /* * jsTree contextmenu plugin */ (function ($) { $.vakata.context = { hide_on_mouseleave : false, cnt : $("<div id='vakata-contextmenu' />"), vis : false, tgt : false, par : false, func : false, data : false, rtl : false, show : function (s, t, x, y, d, p, rtl) { $.vakata.context.rtl = !!rtl; var html = $.vakata.context.parse(s), h, w; if(!html) { return; } $.vakata.context.vis = true; $.vakata.context.tgt = t; $.vakata.context.par = p || t || null; $.vakata.context.data = d || null; $.vakata.context.cnt .html(html) .css({ "visibility" : "hidden", "display" : "block", "left" : 0, "top" : 0 }); if($.vakata.context.hide_on_mouseleave) { $.vakata.context.cnt .one("mouseleave", function(e) { $.vakata.context.hide(); }); } h = $.vakata.context.cnt.height(); w = $.vakata.context.cnt.width(); if(x + w > $(document).width()) { x = $(document).width() - (w + 5); $.vakata.context.cnt.find("li > ul").addClass("right"); } if(y + h > $(document).height()) { y = y - (h + t[0].offsetHeight); $.vakata.context.cnt.find("li > ul").addClass("bottom"); } $.vakata.context.cnt .css({ "left" : x, "top" : y }) .find("li:has(ul)") .bind("mouseenter", function (e) { var w = $(document).width(), h = $(document).height(), ul = $(this).children("ul").show(); if(w !== $(document).width()) { ul.toggleClass("right"); } if(h !== $(document).height()) { ul.toggleClass("bottom"); } }) .bind("mouseleave", function (e) { $(this).children("ul").hide(); }) .end() .css({ "visibility" : "visible" }) .show(); $(document).triggerHandler("context_show.vakata"); }, hide : function () { $.vakata.context.vis = false; $.vakata.context.cnt.attr("class","").css({ "visibility" : "hidden" }); $(document).triggerHandler("context_hide.vakata"); }, parse : function (s, is_callback) { if(!s) { return false; } var str = "", tmp = false, was_sep = true; if(!is_callback) { $.vakata.context.func = {}; } str += "<ul>"; $.each(s, function (i, val) { if(!val) { return true; } $.vakata.context.func[i] = val.action; if(!was_sep && val.separator_before) { str += "<li class='vakata-separator vakata-separator-before'></li>"; } was_sep = false; str += "<li class='" + (val._class || "") + (val._disabled ? " jstree-contextmenu-disabled " : "") + "'><ins "; if(val.icon && val.icon.indexOf("/") === -1) { str += " class='" + val.icon + "' "; } if(val.icon && val.icon.indexOf("/") !== -1) { str += " style='background:url(" + val.icon + ") center center no-repeat;' "; } str += "> </ins><a href='#' rel='" + i + "'>"; if(val.submenu) { str += "<span style='float:" + ($.vakata.context.rtl ? "left" : "right") + ";'>»</span>"; } str += val.label + "</a>"; if(val.submenu) { tmp = $.vakata.context.parse(val.submenu, true); if(tmp) { str += tmp; } } str += "</li>"; if(val.separator_after) { str += "<li class='vakata-separator vakata-separator-after'></li>"; was_sep = true; } }); str = str.replace(/<li class\='vakata-separator vakata-separator-after'\><\/li\>$/,""); str += "</ul>"; $(document).triggerHandler("context_parse.vakata"); return str.length > 10 ? str : false; }, exec : function (i) { if($.isFunction($.vakata.context.func[i])) { // if is string - eval and call it! $.vakata.context.func[i].call($.vakata.context.data, $.vakata.context.par); return true; } else { return false; } } }; $(function () { var css_string = '' + '#vakata-contextmenu { display:block; visibility:hidden; left:0; top:-200px; position:absolute; margin:0; padding:0; min-width:180px; background:#ebebeb; border:1px solid silver; z-index:10000; *width:180px; } ' + '#vakata-contextmenu ul { min-width:180px; *width:180px; } ' + '#vakata-contextmenu ul, #vakata-contextmenu li { margin:0; padding:0; list-style-type:none; display:block; } ' + '#vakata-contextmenu li { line-height:20px; min-height:20px; position:relative; padding:0px; } ' + '#vakata-contextmenu li a { padding:1px 6px; line-height:17px; display:block; text-decoration:none; margin:1px 1px 0 1px; } ' + '#vakata-contextmenu li ins { float:left; width:16px; height:16px; text-decoration:none; margin-right:2px; } ' + '#vakata-contextmenu li a:hover, #vakata-contextmenu li.vakata-hover > a { background:gray; color:white; } ' + '#vakata-contextmenu li ul { display:none; position:absolute; top:-2px; left:100%; background:#ebebeb; border:1px solid gray; } ' + '#vakata-contextmenu .right { right:100%; left:auto; } ' + '#vakata-contextmenu .bottom { bottom:-1px; top:auto; } ' + '#vakata-contextmenu li.vakata-separator { min-height:0; height:1px; line-height:1px; font-size:1px; overflow:hidden; margin:0 2px; background:silver; /* border-top:1px solid #fefefe; */ padding:0; } '; $.vakata.css.add_sheet({ str : css_string, title : "vakata" }); $.vakata.context.cnt .delegate("a","click", function (e) { e.preventDefault(); }) .delegate("a","mouseup", function (e) { if(!$(this).parent().hasClass("jstree-contextmenu-disabled") && $.vakata.context.exec($(this).attr("rel"))) { $.vakata.context.hide(); } else { $(this).blur(); } }) .delegate("a","mouseover", function () { $.vakata.context.cnt.find(".vakata-hover").removeClass("vakata-hover"); }) .appendTo("body"); $(document).bind("mousedown", function (e) { if($.vakata.context.vis && !$.contains($.vakata.context.cnt[0], e.target)) { $.vakata.context.hide(); } }); if(typeof $.hotkeys !== "undefined") { $(document) .bind("keydown", "up", function (e) { if($.vakata.context.vis) { var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").prevAll("li:not(.vakata-separator)").first(); if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").last(); } o.addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "down", function (e) { if($.vakata.context.vis) { var o = $.vakata.context.cnt.find("ul:visible").last().children(".vakata-hover").removeClass("vakata-hover").nextAll("li:not(.vakata-separator)").first(); if(!o.length) { o = $.vakata.context.cnt.find("ul:visible").last().children("li:not(.vakata-separator)").first(); } o.addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "right", function (e) { if($.vakata.context.vis) { $.vakata.context.cnt.find(".vakata-hover").children("ul").show().children("li:not(.vakata-separator)").removeClass("vakata-hover").first().addClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "left", function (e) { if($.vakata.context.vis) { $.vakata.context.cnt.find(".vakata-hover").children("ul").hide().children(".vakata-separator").removeClass("vakata-hover"); e.stopImmediatePropagation(); e.preventDefault(); } }) .bind("keydown", "esc", function (e) { $.vakata.context.hide(); e.preventDefault(); }) .bind("keydown", "space", function (e) { $.vakata.context.cnt.find(".vakata-hover").last().children("a").click(); e.preventDefault(); }); } }); $.jstree.plugin("contextmenu", { __init : function () { this.get_container() .delegate("a", "contextmenu.jstree", $.proxy(function (e) { e.preventDefault(); if(!$(e.currentTarget).hasClass("jstree-loading")) { this.show_contextmenu(e.currentTarget, e.pageX, e.pageY); } }, this)) .delegate("a", "click.jstree", $.proxy(function (e) { if(this.data.contextmenu) { $.vakata.context.hide(); } }, this)) .bind("destroy.jstree", $.proxy(function () { // TODO: move this to descruct method if(this.data.contextmenu) { $.vakata.context.hide(); } }, this)); $(document).bind("context_hide.vakata", $.proxy(function () { this.data.contextmenu = false; }, this)); }, defaults : { select_node : false, // requires UI plugin show_at_node : true, items : { // Could be a function that should return an object like this one "create" : { "separator_before" : false, "separator_after" : true, "label" : "Create", "action" : function (obj) { this.create(obj); } }, "rename" : { "separator_before" : false, "separator_after" : false, "label" : "Rename", "action" : function (obj) { this.rename(obj); } }, "remove" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Delete", "action" : function (obj) { if(this.is_selected(obj)) { this.remove(); } else { this.remove(obj); } } }, "ccp" : { "separator_before" : true, "icon" : false, "separator_after" : false, "label" : "Edit", "action" : false, "submenu" : { "cut" : { "separator_before" : false, "separator_after" : false, "label" : "Cut", "action" : function (obj) { this.cut(obj); } }, "copy" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Copy", "action" : function (obj) { this.copy(obj); } }, "paste" : { "separator_before" : false, "icon" : false, "separator_after" : false, "label" : "Paste", "action" : function (obj) { this.paste(obj); } } } } } }, _fn : { show_contextmenu : function (obj, x, y) { obj = this._get_node(obj); var s = this.get_settings().contextmenu, a = obj.children("a:visible:eq(0)"), o = false, i = false; if(s.select_node && this.data.ui && !this.is_selected(obj)) { this.deselect_all(); this.select_node(obj, true); } if(s.show_at_node || typeof x === "undefined" || typeof y === "undefined") { o = a.offset(); x = o.left; y = o.top + this.data.core.li_height; } i = obj.data("jstree") && obj.data("jstree").contextmenu ? obj.data("jstree").contextmenu : s.items; if($.isFunction(i)) { i = i.call(this, obj); } this.data.contextmenu = true; $.vakata.context.show(i, a, x, y, this, obj, this._get_settings().core.rtl); if(this.data.themes) { $.vakata.context.cnt.attr("class", "jstree-" + this.data.themes.theme + "-context"); } } } }); })(jQuery); //*/ /* * jsTree types plugin * Adds support types of nodes * You can set an attribute on each li node, that represents its type. * According to the type setting the node may get custom icon/validation rules */ (function ($) { $.jstree.plugin("types", { __init : function () { var s = this._get_settings().types; this.data.types.attach_to = []; this.get_container() .bind("init.jstree", $.proxy(function () { var types = s.types, attr = s.type_attr, icons_css = "", _this = this; $.each(types, function (i, tp) { $.each(tp, function (k, v) { if(!/^(max_depth|max_children|icon|valid_children)$/.test(k)) { _this.data.types.attach_to.push(k); } }); if(!tp.icon) { return true; } if( tp.icon.image || tp.icon.position) { if(i == "default") { icons_css += '.jstree-' + _this.get_index() + ' a > .jstree-icon { '; } else { icons_css += '.jstree-' + _this.get_index() + ' li[' + attr + '="' + i + '"] > a > .jstree-icon { '; } if(tp.icon.image) { icons_css += ' background-image:url(' + tp.icon.image + '); '; } if(tp.icon.position){ icons_css += ' background-position:' + tp.icon.position + '; '; } else { icons_css += ' background-position:0 0; '; } icons_css += '} '; } }); if(icons_css !== "") { $.vakata.css.add_sheet({ 'str' : icons_css, title : "jstree-types" }); } }, this)) .bind("before.jstree", $.proxy(function (e, data) { var s, t, o = this._get_settings().types.use_data ? this._get_node(data.args[0]) : false, d = o && o !== -1 && o.length ? o.data("jstree") : false; if(d && d.types && d.types[data.func] === false) { e.stopImmediatePropagation(); return false; } if($.inArray(data.func, this.data.types.attach_to) !== -1) { if(!data.args[0] || (!data.args[0].tagName && !data.args[0].jquery)) { return; } s = this._get_settings().types.types; t = this._get_type(data.args[0]); if( ( (s[t] && typeof s[t][data.func] !== "undefined") || (s["default"] && typeof s["default"][data.func] !== "undefined") ) && this._check(data.func, data.args[0]) === false ) { e.stopImmediatePropagation(); return false; } } }, this)); if(is_ie6) { this.get_container() .bind("load_node.jstree set_type.jstree", $.proxy(function (e, data) { var r = data && data.rslt && data.rslt.obj && data.rslt.obj !== -1 ? this._get_node(data.rslt.obj).parent() : this.get_container_ul(), c = false, s = this._get_settings().types; $.each(s.types, function (i, tp) { if(tp.icon && (tp.icon.image || tp.icon.position)) { c = i === "default" ? r.find("li > a > .jstree-icon") : r.find("li[" + s.type_attr + "='" + i + "'] > a > .jstree-icon"); if(tp.icon.image) { c.css("backgroundImage","url(" + tp.icon.image + ")"); } c.css("backgroundPosition", tp.icon.position || "0 0"); } }); }, this)); } }, defaults : { // defines maximum number of root nodes (-1 means unlimited, -2 means disable max_children checking) max_children : -1, // defines the maximum depth of the tree (-1 means unlimited, -2 means disable max_depth checking) max_depth : -1, // defines valid node types for the root nodes valid_children : "all", // whether to use $.data use_data : false, // where is the type stores (the rel attribute of the LI element) type_attr : "rel", // a list of types types : { // the default type "default" : { "max_children" : -1, "max_depth" : -1, "valid_children": "all" // Bound functions - you can bind any other function here (using boolean or function) //"select_node" : true } } }, _fn : { _types_notify : function (n, data) { if(data.type && this._get_settings().types.use_data) { this.set_type(data.type, n); } }, _get_type : function (obj) { obj = this._get_node(obj); return (!obj || !obj.length) ? false : obj.attr(this._get_settings().types.type_attr) || "default"; }, set_type : function (str, obj) { obj = this._get_node(obj); var ret = (!obj.length || !str) ? false : obj.attr(this._get_settings().types.type_attr, str); if(ret) { this.__callback({ obj : obj, type : str}); } return ret; }, _check : function (rule, obj, opts) { obj = this._get_node(obj); var v = false, t = this._get_type(obj), d = 0, _this = this, s = this._get_settings().types, data = false; if(obj === -1) { if(!!s[rule]) { v = s[rule]; } else { return; } } else { if(t === false) { return; } data = s.use_data ? obj.data("jstree") : false; if(data && data.types && typeof data.types[rule] !== "undefined") { v = data.types[rule]; } else if(!!s.types[t] && typeof s.types[t][rule] !== "undefined") { v = s.types[t][rule]; } else if(!!s.types["default"] && typeof s.types["default"][rule] !== "undefined") { v = s.types["default"][rule]; } } if($.isFunction(v)) { v = v.call(this, obj); } if(rule === "max_depth" && obj !== -1 && opts !== false && s.max_depth !== -2 && v !== 0) { // also include the node itself - otherwise if root node it is not checked obj.children("a:eq(0)").parentsUntil(".jstree","li").each(function (i) { // check if current depth already exceeds global tree depth if(s.max_depth !== -1 && s.max_depth - (i + 1) <= 0) { v = 0; return false; } d = (i === 0) ? v : _this._check(rule, this, false); // check if current node max depth is already matched or exceeded if(d !== -1 && d - (i + 1) <= 0) { v = 0; return false; } // otherwise - set the max depth to the current value minus current depth if(d >= 0 && (d - (i + 1) < v || v < 0) ) { v = d - (i + 1); } // if the global tree depth exists and it minus the nodes calculated so far is less than `v` or `v` is unlimited if(s.max_depth >= 0 && (s.max_depth - (i + 1) < v || v < 0) ) { v = s.max_depth - (i + 1); } }); } return v; }, check_move : function () { if(!this.__call_old()) { return false; } var m = this._get_move(), s = m.rt._get_settings().types, mc = m.rt._check("max_children", m.cr), md = m.rt._check("max_depth", m.cr), vc = m.rt._check("valid_children", m.cr), ch = 0, d = 1, t; if(vc === "none") { return false; } if($.isArray(vc) && m.ot && m.ot._get_type) { m.o.each(function () { if($.inArray(m.ot._get_type(this), vc) === -1) { d = false; return false; } }); if(d === false) { return false; } } if(s.max_children !== -2 && mc !== -1) { ch = m.cr === -1 ? this.get_container().find("> ul > li").not(m.o).length : m.cr.find("> ul > li").not(m.o).length; if(ch + m.o.length > mc) { return false; } } if(s.max_depth !== -2 && md !== -1) { d = 0; if(md === 0) { return false; } if(typeof m.o.d === "undefined") { // TODO: deal with progressive rendering and async when checking max_depth (how to know the depth of the moved node) t = m.o; while(t.length > 0) { t = t.find("> ul > li"); d ++; } m.o.d = d; } if(md - m.o.d < 0) { return false; } } return true; }, create_node : function (obj, position, js, callback, is_loaded, skip_check) { if(!skip_check && (is_loaded || this._is_loaded(obj))) { var p = (typeof position == "string" && position.match(/^before|after$/i) && obj !== -1) ? this._get_parent(obj) : this._get_node(obj), s = this._get_settings().types, mc = this._check("max_children", p), md = this._check("max_depth", p), vc = this._check("valid_children", p), ch; if(typeof js === "string") { js = { data : js }; } if(!js) { js = {}; } if(vc === "none") { return false; } if($.isArray(vc)) { if(!js.attr || !js.attr[s.type_attr]) { if(!js.attr) { js.attr = {}; } js.attr[s.type_attr] = vc[0]; } else { if($.inArray(js.attr[s.type_attr], vc) === -1) { return false; } } } if(s.max_children !== -2 && mc !== -1) { ch = p === -1 ? this.get_container().find("> ul > li").length : p.find("> ul > li").length; if(ch + 1 > mc) { return false; } } if(s.max_depth !== -2 && md !== -1 && (md - 1) < 0) { return false; } } return this.__call_old(true, obj, position, js, callback, is_loaded, skip_check); } } }); })(jQuery); //*/ /* * jsTree HTML plugin * The HTML data store. Datastores are build by replacing the `load_node` and `_is_loaded` functions. */ (function ($) { $.jstree.plugin("html_data", { __init : function () { // this used to use html() and clean the whitespace, but this way any attached data was lost this.data.html_data.original_container_html = this.get_container().find(" > ul > li").clone(true); // remove white space from LI node - otherwise nodes appear a bit to the right this.data.html_data.original_container_html.find("li").andSelf().contents().filter(function() { return this.nodeType == 3; }).remove(); }, defaults : { data : false, ajax : false, correct_state : true }, _fn : { load_node : function (obj, s_call, e_call) { var _this = this; this.load_node_html(obj, function () { _this.__callback({ "obj" : _this._get_node(obj) }); s_call.call(this); }, e_call); }, _is_loaded : function (obj) { obj = this._get_node(obj); return obj == -1 || !obj || (!this._get_settings().html_data.ajax && !$.isFunction(this._get_settings().html_data.data)) || obj.is(".jstree-open, .jstree-leaf") || obj.children("ul").children("li").size() > 0; }, load_node_html : function (obj, s_call, e_call) { var d, s = this.get_settings().html_data, error_func = function () {}, success_func = function () {}; obj = this._get_node(obj); if(obj && obj !== -1) { if(obj.data("jstree_is_loading")) { return; } else { obj.data("jstree_is_loading",true); } } switch(!0) { case ($.isFunction(s.data)): s.data.call(this, obj, $.proxy(function (d) { if(d && d !== "" && d.toString && d.toString().replace(/^[\s\n]+$/,"") !== "") { d = $(d); if(!d.is("ul")) { d = $("<ul />").append(d); } if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } }, this)); break; case (!s.data && !s.ajax): if(!obj || obj == -1) { this.get_container() .children("ul").empty() .append(this.data.html_data.original_container_html) .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end() .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); this.clean_node(); } if(s_call) { s_call.call(this); } break; case (!!s.data && !s.ajax) || (!!s.data && !!s.ajax && (!obj || obj === -1)): if(!obj || obj == -1) { d = $(s.data); if(!d.is("ul")) { d = $("<ul />").append(d); } this.get_container() .children("ul").empty().append(d.children()) .find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end() .filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); this.clean_node(); } if(s_call) { s_call.call(this); } break; case (!s.data && !!s.ajax) || (!!s.data && !!s.ajax && obj && obj !== -1): obj = this._get_node(obj); error_func = function (x, t, e) { var ef = this.get_settings().html_data.ajax.error; if(ef) { ef.call(this, x, t, e); } if(obj != -1 && obj.length) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(t === "success" && s.correct_state) { this.correct_state(obj); } } else { if(t === "success" && s.correct_state) { this.get_container().children("ul").empty(); } } if(e_call) { e_call.call(this); } }; success_func = function (d, t, x) { var sf = this.get_settings().html_data.ajax.success; if(sf) { d = sf.call(this,d,t,x) || d; } if(d === "" || (d && d.toString && d.toString().replace(/^[\s\n]+$/,"") === "")) { return error_func.call(this, x, t, ""); } if(d) { d = $(d); if(!d.is("ul")) { d = $("<ul />").append(d); } if(obj == -1 || !obj) { this.get_container().children("ul").empty().append(d.children()).find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); } else { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.append(d).children("ul").find("li, a").filter(function () { return !this.firstChild || !this.firstChild.tagName || this.firstChild.tagName !== "INS"; }).prepend("<ins class='jstree-icon'> </ins>").end().filter("a").children("ins:first-child").not(".jstree-icon").addClass("jstree-icon"); obj.removeData("jstree_is_loading"); } this.clean_node(obj); if(s_call) { s_call.call(this); } } else { if(obj && obj !== -1) { obj.children("a.jstree-loading").removeClass("jstree-loading"); obj.removeData("jstree_is_loading"); if(s.correct_state) { this.correct_state(obj); if(s_call) { s_call.call(this); } } } else { if(s.correct_state) { this.get_container().children("ul").empty(); if(s_call) { s_call.call(this); } } } } }; s.ajax.context = this; s.ajax.error = error_func; s.ajax.success = success_func; if(!s.ajax.dataType) { s.ajax.dataType = "html"; } if($.isFunction(s.ajax.url)) { s.ajax.url = s.ajax.url.call(this, obj); } if($.isFunction(s.ajax.data)) { s.ajax.data = s.ajax.data.call(this, obj); } $.ajax(s.ajax); break; } } } }); // include the HTML data plugin by default $.jstree.defaults.plugins.push("html_data"); })(jQuery); //*/ /* * jsTree themeroller plugin * Adds support for jQuery UI themes. Include this at the end of your plugins list, also make sure "themes" is not included. */ (function ($) { $.jstree.plugin("themeroller", { __init : function () { var s = this._get_settings().themeroller; this.get_container() .addClass("ui-widget-content") .addClass("jstree-themeroller") .delegate("a","mouseenter.jstree", function (e) { if(!$(e.currentTarget).hasClass("jstree-loading")) { $(this).addClass(s.item_h); } }) .delegate("a","mouseleave.jstree", function () { $(this).removeClass(s.item_h); }) .bind("init.jstree", $.proxy(function (e, data) { data.inst.get_container().find("> ul > li > .jstree-loading > ins").addClass("ui-icon-refresh"); this._themeroller(data.inst.get_container().find("> ul > li")); }, this)) .bind("open_node.jstree create_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.obj); }, this)) .bind("loaded.jstree refresh.jstree", $.proxy(function (e) { this._themeroller(); }, this)) .bind("close_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.obj); }, this)) .bind("delete_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.parent); }, this)) .bind("correct_state.jstree", $.proxy(function (e, data) { data.rslt.obj .children("ins.jstree-icon").removeClass(s.opened + " " + s.closed + " ui-icon").end() .find("> a > ins.ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_open + " " + s.item_clsd).addClass(s.item_leaf || "jstree-no-icon"); }, this)) .bind("select_node.jstree", $.proxy(function (e, data) { data.rslt.obj.children("a").addClass(s.item_a); }, this)) .bind("deselect_node.jstree deselect_all.jstree", $.proxy(function (e, data) { this.get_container() .find("a." + s.item_a).removeClass(s.item_a).end() .find("a.jstree-clicked").addClass(s.item_a); }, this)) .bind("dehover_node.jstree", $.proxy(function (e, data) { data.rslt.obj.children("a").removeClass(s.item_h); }, this)) .bind("hover_node.jstree", $.proxy(function (e, data) { this.get_container() .find("a." + s.item_h).not(data.rslt.obj).removeClass(s.item_h); data.rslt.obj.children("a").addClass(s.item_h); }, this)) .bind("move_node.jstree", $.proxy(function (e, data) { this._themeroller(data.rslt.o); this._themeroller(data.rslt.op); }, this)); }, __destroy : function () { var s = this._get_settings().themeroller, c = [ "ui-icon" ]; $.each(s, function (i, v) { v = v.split(" "); if(v.length) { c = c.concat(v); } }); this.get_container() .removeClass("ui-widget-content") .find("." + c.join(", .")).removeClass(c.join(" ")); }, _fn : { _themeroller : function (obj) { var s = this._get_settings().themeroller; obj = !obj || obj == -1 ? this.get_container_ul() : this._get_node(obj).parent(); obj .find("li.jstree-closed") .children("ins.jstree-icon").removeClass(s.opened).addClass("ui-icon " + s.closed).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_leaf + " " + s.item_open).addClass(s.item_clsd || "jstree-no-icon") .end() .end() .end() .end() .find("li.jstree-open") .children("ins.jstree-icon").removeClass(s.closed).addClass("ui-icon " + s.opened).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_leaf + " " + s.item_clsd).addClass(s.item_open || "jstree-no-icon") .end() .end() .end() .end() .find("li.jstree-leaf") .children("ins.jstree-icon").removeClass(s.closed + " ui-icon " + s.opened).end() .children("a").addClass(s.item) .children("ins.jstree-icon").addClass("ui-icon") .filter(function() { return this.className.toString() .replace(s.item_clsd,"").replace(s.item_open,"").replace(s.item_leaf,"") .indexOf("ui-icon-") === -1; }).removeClass(s.item_clsd + " " + s.item_open).addClass(s.item_leaf || "jstree-no-icon"); } }, defaults : { "opened" : "ui-icon-triangle-1-se", "closed" : "ui-icon-triangle-1-e", "item" : "ui-state-default", "item_h" : "ui-state-hover", "item_a" : "ui-state-active", "item_open" : "ui-icon-folder-open", "item_clsd" : "ui-icon-folder-collapsed", "item_leaf" : "ui-icon-document" } }); $(function() { var css_string = '' + '.jstree-themeroller .ui-icon { overflow:visible; } ' + '.jstree-themeroller a { padding:0 2px; } ' + '.jstree-themeroller .jstree-no-icon { display:none; }'; $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree unique plugin * Forces different names amongst siblings (still a bit experimental) * NOTE: does not check language versions (it will not be possible to have nodes with the same title, even in different languages) */ (function ($) { $.jstree.plugin("unique", { __init : function () { this.get_container() .bind("before.jstree", $.proxy(function (e, data) { var nms = [], res = true, p, t; if(data.func == "move_node") { // obj, ref, position, is_copy, is_prepared, skip_check if(data.args[4] === true) { if(data.args[0].o && data.args[0].o.length) { data.args[0].o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); }); res = this._check_unique(nms, data.args[0].np.find("> ul > li").not(data.args[0].o), "move_node"); } } } if(data.func == "create_node") { // obj, position, js, callback, is_loaded if(data.args[4] || this._is_loaded(data.args[0])) { p = this._get_node(data.args[0]); if(data.args[1] && (data.args[1] === "before" || data.args[1] === "after")) { p = this._get_parent(data.args[0]); if(!p || p === -1) { p = this.get_container(); } } if(typeof data.args[2] === "string") { nms.push(data.args[2]); } else if(!data.args[2] || !data.args[2].data) { nms.push(this._get_string("new_node")); } else { nms.push(data.args[2].data); } res = this._check_unique(nms, p.find("> ul > li"), "create_node"); } } if(data.func == "rename_node") { // obj, val nms.push(data.args[1]); t = this._get_node(data.args[0]); p = this._get_parent(t); if(!p || p === -1) { p = this.get_container(); } res = this._check_unique(nms, p.find("> ul > li").not(t), "rename_node"); } if(!res) { e.stopPropagation(); return false; } }, this)); }, defaults : { error_callback : $.noop }, _fn : { _check_unique : function (nms, p, func) { var cnms = []; p.children("a").each(function () { cnms.push($(this).text().replace(/^\s+/g,"")); }); if(!cnms.length || !nms.length) { return true; } cnms = cnms.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); if((cnms.length + nms.length) != cnms.concat(nms).sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(",").length) { this._get_settings().unique.error_callback.call(null, nms, p, func); return false; } return true; }, check_move : function () { if(!this.__call_old()) { return false; } var p = this._get_move(), nms = []; if(p.o && p.o.length) { p.o.children("a").each(function () { nms.push($(this).text().replace(/^\s+/g,"")); }); return this._check_unique(nms, p.np.find("> ul > li").not(p.o), "check_move"); } return true; } } }); })(jQuery); //*/ /* * jsTree wholerow plugin * Makes select and hover work on the entire width of the node * MAY BE HEAVY IN LARGE DOM */ (function ($) { $.jstree.plugin("wholerow", { __init : function () { if(!this.data.ui) { throw "jsTree wholerow: jsTree UI plugin not included."; } this.data.wholerow.html = false; this.data.wholerow.to = false; this.get_container() .bind("init.jstree", $.proxy(function (e, data) { this._get_settings().core.animation = 0; }, this)) .bind("open_node.jstree create_node.jstree clean_node.jstree loaded.jstree", $.proxy(function (e, data) { this._prepare_wholerow_span( data && data.rslt && data.rslt.obj ? data.rslt.obj : -1 ); }, this)) .bind("search.jstree clear_search.jstree reopen.jstree after_open.jstree after_close.jstree create_node.jstree delete_node.jstree clean_node.jstree", $.proxy(function (e, data) { if(this.data.to) { clearTimeout(this.data.to); } this.data.to = setTimeout( (function (t, o) { return function() { t._prepare_wholerow_ul(o); }; })(this, data && data.rslt && data.rslt.obj ? data.rslt.obj : -1), 0); }, this)) .bind("deselect_all.jstree", $.proxy(function (e, data) { this.get_container().find(" > .jstree-wholerow .jstree-clicked").removeClass("jstree-clicked " + (this.data.themeroller ? this._get_settings().themeroller.item_a : "" )); }, this)) .bind("select_node.jstree deselect_node.jstree ", $.proxy(function (e, data) { data.rslt.obj.each(function () { var ref = data.inst.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt((($(this).offset().top - data.inst.get_container().offset().top + data.inst.get_container()[0].scrollTop) / data.inst.data.core.li_height),10)) + ")"); // ref.children("a")[e.type === "select_node" ? "addClass" : "removeClass"]("jstree-clicked"); ref.children("a").attr("class",data.rslt.obj.children("a").attr("class")); }); }, this)) .bind("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) { this.get_container().find(" > .jstree-wholerow .jstree-hovered").removeClass("jstree-hovered " + (this.data.themeroller ? this._get_settings().themeroller.item_h : "" )); if(e.type === "hover_node") { var ref = this.get_container().find(" > .jstree-wholerow li:visible:eq(" + ( parseInt(((data.rslt.obj.offset().top - this.get_container().offset().top + this.get_container()[0].scrollTop) / this.data.core.li_height),10)) + ")"); // ref.children("a").addClass("jstree-hovered"); ref.children("a").attr("class",data.rslt.obj.children(".jstree-hovered").attr("class")); } }, this)) .delegate(".jstree-wholerow-span, ins.jstree-icon, li", "click.jstree", function (e) { var n = $(e.currentTarget); if(e.target.tagName === "A" || (e.target.tagName === "INS" && n.closest("li").is(".jstree-open, .jstree-closed"))) { return; } n.closest("li").children("a:visible:eq(0)").click(); e.stopImmediatePropagation(); }) .delegate("li", "mouseover.jstree", $.proxy(function (e) { e.stopImmediatePropagation(); if($(e.currentTarget).children(".jstree-hovered, .jstree-clicked").length) { return false; } this.hover_node(e.currentTarget); return false; }, this)) .delegate("li", "mouseleave.jstree", $.proxy(function (e) { if($(e.currentTarget).children("a").hasClass("jstree-hovered").length) { return; } this.dehover_node(e.currentTarget); }, this)); if(is_ie7 || is_ie6) { $.vakata.css.add_sheet({ str : ".jstree-" + this.get_index() + " { position:relative; } ", title : "jstree" }); } }, defaults : { }, __destroy : function () { this.get_container().children(".jstree-wholerow").remove(); this.get_container().find(".jstree-wholerow-span").remove(); }, _fn : { _prepare_wholerow_span : function (obj) { obj = !obj || obj == -1 ? this.get_container().find("> ul > li") : this._get_node(obj); if(obj === false) { return; } // added for removing root nodes obj.each(function () { $(this).find("li").andSelf().each(function () { var $t = $(this); if($t.children(".jstree-wholerow-span").length) { return true; } $t.prepend("<span class='jstree-wholerow-span' style='width:" + ($t.parentsUntil(".jstree","li").length * 18) + "px;'> </span>"); }); }); }, _prepare_wholerow_ul : function () { var o = this.get_container().children("ul").eq(0), h = o.html(); o.addClass("jstree-wholerow-real"); if(this.data.wholerow.last_html !== h) { this.data.wholerow.last_html = h; this.get_container().children(".jstree-wholerow").remove(); this.get_container().append( o.clone().removeClass("jstree-wholerow-real") .wrapAll("<div class='jstree-wholerow' />").parent() .width(o.parent()[0].scrollWidth) .css("top", (o.height() + ( is_ie7 ? 5 : 0)) * -1 ) .find("li[id]").each(function () { this.removeAttribute("id"); }).end() ); } } } }); $(function() { var css_string = '' + '.jstree .jstree-wholerow-real { position:relative; z-index:1; } ' + '.jstree .jstree-wholerow-real li { cursor:pointer; } ' + '.jstree .jstree-wholerow-real a { border-left-color:transparent !important; border-right-color:transparent !important; } ' + '.jstree .jstree-wholerow { position:relative; z-index:0; height:0; } ' + '.jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { width:100%; } ' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li, .jstree .jstree-wholerow a { margin:0 !important; padding:0 !important; } ' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow li { background:transparent !important; }' + '.jstree .jstree-wholerow ins, .jstree .jstree-wholerow span, .jstree .jstree-wholerow input { display:none !important; }' + '.jstree .jstree-wholerow a, .jstree .jstree-wholerow a:hover { text-indent:-9999px; !important; width:100%; padding:0 !important; border-right-width:0px !important; border-left-width:0px !important; } ' + '.jstree .jstree-wholerow-span { position:absolute; left:0; margin:0px; padding:0; height:18px; border-width:0; padding:0; z-index:0; }'; if(is_ff2) { css_string += '' + '.jstree .jstree-wholerow a { display:block; height:18px; margin:0; padding:0; border:0; } ' + '.jstree .jstree-wholerow-real a { border-color:transparent !important; } '; } if(is_ie7 || is_ie6) { css_string += '' + '.jstree .jstree-wholerow, .jstree .jstree-wholerow li, .jstree .jstree-wholerow ul, .jstree .jstree-wholerow a { margin:0; padding:0; line-height:18px; } ' + '.jstree .jstree-wholerow a { display:block; height:18px; line-height:18px; overflow:hidden; } '; } $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); }); })(jQuery); //*/ /* * jsTree model plugin * This plugin gets jstree to use a class model to retrieve data, creating great dynamism */ (function ($) { var nodeInterface = ["getChildren","getChildrenCount","getAttr","getName","getProps"], validateInterface = function(obj, inter) { var valid = true; obj = obj || {}; inter = [].concat(inter); $.each(inter, function (i, v) { if(!$.isFunction(obj[v])) { valid = false; return false; } }); return valid; }; $.jstree.plugin("model", { __init : function () { if(!this.data.json_data) { throw "jsTree model: jsTree json_data plugin not included."; } this._get_settings().json_data.data = function (n, b) { var obj = (n == -1) ? this._get_settings().model.object : n.data("jstree_model"); if(!validateInterface(obj, nodeInterface)) { return b.call(null, false); } if(this._get_settings().model.async) { obj.getChildren($.proxy(function (data) { this.model_done(data, b); }, this)); } else { this.model_done(obj.getChildren(), b); } }; }, defaults : { object : false, id_prefix : false, async : false }, _fn : { model_done : function (data, callback) { var ret = [], s = this._get_settings(), _this = this; if(!$.isArray(data)) { data = [data]; } $.each(data, function (i, nd) { var r = nd.getProps() || {}; r.attr = nd.getAttr() || {}; if(nd.getChildrenCount()) { r.state = "closed"; } r.data = nd.getName(); if(!$.isArray(r.data)) { r.data = [r.data]; } if(_this.data.types && $.isFunction(nd.getType)) { r.attr[s.types.type_attr] = nd.getType(); } if(r.attr.id && s.model.id_prefix) { r.attr.id = s.model.id_prefix + r.attr.id; } if(!r.metadata) { r.metadata = { }; } r.metadata.jstree_model = nd; ret.push(r); }); callback.call(null, ret); } } }); })(jQuery); //*/ })(); define("thirdparty/jstree_pre1.0_fix_1/jquery.jstree", function(){}); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * PreferenceStorage defines an interface for persisting preference data as * name/value pairs for a module or plugin. */ define('preferences/PreferenceStorage',['require','exports','module','preferences/PreferencesManager'],function (require, exports, module) { var PreferencesManager = require("preferences/PreferencesManager"); /** * @private * Validate JSON keys and values. */ function _validateJSONPair(key, value) { if (typeof key === "string") { // validate temporary JSON var temp = {}, error = null; temp[key] = value; try { temp = JSON.parse(JSON.stringify(temp)); } catch (err) { error = err; } // set value to JSON storage if no errors occurred if (!error && (temp[key] !== undefined)) { return true; } else { throw new Error("Value '" + value + "' for key '" + key + "' must be a valid JSON value"); } } else { throw new Error("Preference key '" + key + "' must be a string"); } } /** * @private * Save to persistent storage. */ function _commit() { PreferencesManager.savePreferences(); } /** * Creates a new PreferenceStorage object. * @param {!string} clientID Unique identifier for PreferencesManager to * associate this PreferenceStorage data with. * @param {!object} json JSON object to persist preference data. */ function PreferenceStorage(clientID, json) { this._clientID = clientID; this._json = json; } /** * Unique clientID for this PreferenceStorage object. * @return {!string} clientID */ PreferenceStorage.prototype.getClientID = function () { return this._clientID; }; /** * Removes a preference from this PreferenceStorage object. * @param {!string} key A unique identifier */ PreferenceStorage.prototype.remove = function (key) { // remove value from JSON storage delete this._json[key]; _commit(); }; /** * Assigns a value for a key. Overwrites existing value if present. * @param {!string} key A unique identifier * @param {object} value A valid JSON value */ PreferenceStorage.prototype.setValue = function (key, value) { if (_validateJSONPair(key, value)) { this._json[key] = value; _commit(); } }; /** * Retreive the value associated with the specified key. * @param {!string} key Key name to lookup. * @return {object} Returns the value for the key or undefined. */ PreferenceStorage.prototype.getValue = function (key) { return this._json[key]; }; /** * Return all name-value pairs as a single JSON object. * @return {!object} JSON object containing name/value pairs for all keys * in this PreferenceStorage object. */ PreferenceStorage.prototype.getAllValues = function () { return JSON.parse(JSON.stringify(this._json)); }; /** * Writes name-value pairs from a JSON object as preference properties. * Invalid JSON values throw an error and all changes are discarded. * * @param {!object} obj A JSON object with zero or more preference properties to write. * @param {boolean} append Defaults to false. When true, properties in the JSON object * overwrite and/or append to the existing set of preference properties. When false, * all existing preferences are deleted before writing new properties from the JSON object. */ PreferenceStorage.prototype.setAllValues = function (obj, append) { var self = this, error = null; // validate all name/value pairs before committing $.each(obj, function (key, value) { try { _validateJSONPair(key, value); } catch (err) { // fail fast error = err; return false; } }); // skip changes if any error is detected if (error) { throw error; } // delete all exiting properties if not appending if (!append) { $.each(this._json, function (key, value) { delete self._json[key]; }); } // copy properties from incoming JSON object $.each(obj, function (key, value) { self._json[key] = value; }); _commit(); }; exports.PreferenceStorage = PreferenceStorage; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, localStorage */ /** * PreferencesManager * */ define('preferences/PreferencesManager',['require','exports','module','preferences/PreferenceStorage'],function (require, exports, module) { var PreferenceStorage = require("preferences/PreferenceStorage").PreferenceStorage; var PREFERENCES_KEY = "com.adobe.brackets.preferences"; // Private Properties var preferencesKey, prefStorage, persistentStorage, doLoadPreferences = false; /** * Retreive preferences data for the given clientID. * * @param {string} clientID Unique identifier * @param {string} defaults Default preferences stored as JSON * @return {PreferenceStorage} */ function getPreferenceStorage(clientID, defaults) { if ((clientID === undefined) || (clientID === null)) { throw new Error("Invalid clientID"); } var prefs = prefStorage[clientID]; if (prefs === undefined) { // create a new empty preferences object prefs = (defaults && JSON.stringify(defaults)) ? defaults : {}; prefStorage[clientID] = prefs; } return new PreferenceStorage(clientID, prefs); } /** * Save all preference clients. */ function savePreferences() { // save all preferences persistentStorage.setItem(preferencesKey, JSON.stringify(prefStorage)); } /** * @private * Reset preferences and callbacks */ function _reset() { prefStorage = {}; // Note that storage.clear() is not used. Production and unit test code // both rely on the same backing storage but unique item keys. persistentStorage.setItem(preferencesKey, JSON.stringify(prefStorage)); } /** * @private * Initialize persistent storage implementation */ function _initStorage(storage) { persistentStorage = storage; if (doLoadPreferences) { prefStorage = JSON.parse(persistentStorage.getItem(preferencesKey)); } // initialize empty preferences if none were found in storage if (!prefStorage) { _reset(); } } // Check localStorage for a preferencesKey. Production and unit test keys // are used to keep preferences separate within the same storage implementation. preferencesKey = localStorage.getItem("preferencesKey"); if (!preferencesKey) { // use default key if none is found preferencesKey = PREFERENCES_KEY; doLoadPreferences = true; } else { // using a non-default key, check for additional settings doLoadPreferences = !!(localStorage.getItem("doLoadPreferences")); } // Use localStorage by default _initStorage(localStorage); // Public API exports.getPreferenceStorage = getPreferenceStorage; exports.savePreferences = savePreferences; // Unit test use only exports._reset = _reset; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Manages global application commands that can be called from menu items, key bindings, or subparts * of the application. */ define('command/CommandManager',['require','exports','module'],function (require, exports, module) { /** * Map of all registered global commands * @type Object.<commandID: string, Command> */ var _commands = {}; /** * Temporary copy of commands map for restoring after testing * TODO (issue #1039): implement separate require contexts for unit tests * @type Object.<commandID: string, Command> */ var _commandsOriginal = {}; /** * @constructor * @private * * @param {string} name - text that will be displayed in the UI to represent command * @param {string} id * @param {function} commandFn - the function that is called when the command is executed. * * TODO: where should this be triggered, The Command or Exports? * Events: * enabledStateChange * checkedStateChange * keyBindingAdded * keyBindingRemoved */ function Command(name, id, commandFn) { this._name = name; this._id = id; this._commandFn = commandFn; this._checked = undefined; this._enabled = true; } /** @return {Command} */ Command.prototype.getID = function () { return this._id; }; /** * Executes the command. Additional arguments are passed to the executing function * * @return {$.Promise} a jQuery promise that will be resolved when the command completes. */ Command.prototype.execute = function () { if (!this._enabled) { return; } var result = this._commandFn.apply(this, arguments); if (!result) { return (new $.Deferred()).resolve().promise(); } else { return result; } }; /** @return {boolean} */ Command.prototype.getEnabled = function () { return this._enabled; }; /** * Sets enabled state of Command and dispatches "enabledStateChange" * when the enabled state changes. * @param {boolean} enabled */ Command.prototype.setEnabled = function (enabled) { var changed = this._enabled !== enabled; this._enabled = enabled; if (changed) { $(this).triggerHandler("enabledStateChange"); } }; /** * Sets enabled state of Command and dispatches "checkedStateChange" * when the enabled state changes. * @param {boolean} checked */ Command.prototype.setChecked = function (checked) { var changed = this._checked !== checked; this._checked = checked; if (changed) { $(this).triggerHandler("checkedStateChange"); } }; /** @return {boolean} */ Command.prototype.getChecked = function () { return this._checked; }; /** * Sets the name of the Command and dispatches "nameChange" so that * UI that reflects the command name can update. * * Note, a Command name can appear in either HTML or native UI * so HTML tags should not be used. To add a Unicode character, * use \uXXXX instead of an HTML entity. * * @param {string} name */ Command.prototype.setName = function (name) { var changed = this._name !== name; this._name = name; if (changed) { $(this).triggerHandler("nameChange"); } }; /** @return {string} */ Command.prototype.getName = function () { return this._name; }; /** * Registers a global command. * @param {string} name - text that will be displayed in the UI to represent command * @param {string} id - unique identifier for command. * Core commands in Brackets use a simple command title as an id, for example "open.file". * Extensions should use the following format: "author.myextension.mycommandname". * For example, "lschmitt.csswizard.format.css". * @param {function(...)} commandFn - the function to call when the command is executed. Any arguments passed to * execute() (after the id) are passed as arguments to the function. If the function is asynchronous, * it must return a jQuery promise that is resolved when the command completes. Otherwise, the * CommandManager will assume it is synchronous, and return a promise that is already resolved. * @return {?Command} */ function register(name, id, commandFn) { if (_commands[id]) { console.log("Attempting to register an already-registered command: " + id); return null; } if (!name || !id || !commandFn) { throw new Error("Attempting to register a command with a missing name, id, or command function:" + name + " " + id); } var command = new Command(name, id, commandFn); _commands[id] = command; return command; } /** * Clear all commands for unit testing, but first make copy of commands so that * they can be restored afterward */ function _testReset() { _commandsOriginal = _commands; _commands = {}; } /** * Restore original commands after test and release copy */ function _testRestore() { _commands = _commandsOriginal; _commandsOriginal = {}; } /** * Retrieves a Command object by id * @param {string} id * @return {Command} */ function get(id) { return _commands[id]; } /** * Returns the ids of all registered commands * @return {Array.<string>} */ function getAll() { return Object.keys(_commands); } /** * Looks up and runs a global command. Additional arguments are passed to the command. * * @param {string} id The ID of the command to run. * @return {$.Promise} a jQuery promise that will be resolved when the command completes. */ function execute(id) { var command = _commands[id]; if (command) { return command.execute.apply(command, Array.prototype.slice.call(arguments, 1)); } else { return (new $.Deferred()).reject().promise(); } } // Define public API exports.register = register; exports.execute = execute; exports.get = get; exports.getAll = getAll; exports._testReset = _testReset; exports._testRestore = _testRestore; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('command/Commands',['require','exports','module'],function (require, exports, module) { /** * List of constants for global command IDs. */ // FILE exports.FILE_NEW = "file.new"; exports.FILE_OPEN = "file.open"; exports.FILE_OPEN_FOLDER = "file.openFolder"; exports.FILE_SAVE = "file.save"; exports.FILE_SAVE_ALL = "file.saveAll"; exports.FILE_CLOSE = "file.close"; exports.FILE_CLOSE_ALL = "file.close_all"; exports.FILE_CLOSE_WINDOW = "file.close_window"; // string must MATCH string in native code (brackets_extensions) exports.FILE_ADD_TO_WORKING_SET = "file.addToWorkingSet"; exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview"; exports.FILE_QUIT = "file.quit"; // string must MATCH string in native code (brackets_extensions) // EDIT exports.EDIT_UNDO = "edit.undo"; exports.EDIT_REDO = "edit.redo"; exports.EDIT_CUT = "edit.cut"; exports.EDIT_COPY = "edit.copy"; exports.EDIT_PASTE = "edit.paste"; exports.EDIT_SELECT_ALL = "edit.selectAll"; exports.EDIT_FIND = "edit.find"; exports.EDIT_FIND_IN_FILES = "edit.findInFiles"; exports.EDIT_FIND_NEXT = "edit.findNext"; exports.EDIT_FIND_PREVIOUS = "edit.findPrevious"; exports.EDIT_REPLACE = "edit.replace"; exports.EDIT_INDENT = "edit.indent"; exports.EDIT_UNINDENT = "edit.unindent"; exports.EDIT_DUPLICATE = "edit.duplicate"; exports.EDIT_LINE_COMMENT = "edit.lineComment"; exports.EDIT_LINE_UP = "edit.lineUp"; exports.EDIT_LINE_DOWN = "edit.lineDown"; exports.TOGGLE_USE_TAB_CHARS = "debug.useTabChars"; // VIEW exports.VIEW_HIDE_SIDEBAR = "view.toggleSidebar"; exports.VIEW_INCREASE_FONT_SIZE = "view.increaseFontSize"; exports.VIEW_DECREASE_FONT_SIZE = "view.decreaseFontSize"; exports.VIEW_RESTORE_FONT_SIZE = "view.restoreFontSize"; exports.TOGGLE_JSLINT = "debug.jslint"; // Navigate exports.NAVIGATE_NEXT_DOC = "navigate.nextDoc"; exports.NAVIGATE_PREV_DOC = "navigate.prevDoc"; exports.NAVIGATE_QUICK_OPEN = "navigate.quickOpen"; exports.NAVIGATE_GOTO_DEFINITION = "navigate.gotoDefinition"; exports.NAVIGATE_GOTO_LINE = "navigate.gotoLine"; exports.TOGGLE_QUICK_EDIT = "navigate.toggleQuickEdit"; exports.QUICK_EDIT_NEXT_MATCH = "navigate.nextMatch"; exports.QUICK_EDIT_PREV_MATCH = "navigate.previousMatch"; // Debug exports.DEBUG_REFRESH_WINDOW = "debug.refreshWindow"; // string must MATCH string in native code (brackets_extensions) exports.DEBUG_SHOW_DEVELOPER_TOOLS = "debug.showDeveloperTools"; exports.DEBUG_RUN_UNIT_TESTS = "debug.runUnitTests"; exports.DEBUG_SHOW_PERF_DATA = "debug.showPerfData"; exports.DEBUG_NEW_BRACKETS_WINDOW = "debug.newBracketsWindow"; exports.DEBUG_SHOW_EXT_FOLDER = "debug.showExtensionsFolder"; exports.DEBUG_SWITCH_LANGUAGE = "debug.switchLanguage"; exports.CHECK_FOR_UPDATE = "app.checkForUpdate"; // Command that does nothing. Can be used for place holder menuItems exports.HELP_ABOUT = "help.about"; // File shell callbacks exports.APP_ABORT_QUIT = "app.abort_quit"; // string must MATCH string in native code (appshell_extensions) }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */ /*global define, $, brackets, window */ /** * Manages the mapping of keyboard inputs to commands. */ define('command/KeyBindingManager',['require','exports','module','command/CommandManager'],function (require, exports, module) { var CommandManager = require("command/CommandManager"); /** * Maps normalized shortcut descriptor to key binding info. * @type {!Object.<string, {commandID: string, key: string, displayKey: string}>} */ var _keyMap = {}; /** * Maps commandID to the list of shortcuts that are bound to it. * @type {!Object.<string, Array.<{key: string, displayKey: string}>>} */ var _commandMap = {}; /** * Allow clients to toggle key binding */ var _enabled = true; /** * @private */ function _reset() { _keyMap = {}; _commandMap = {}; } /** * @private * Initialize an empty keymap as the current keymap. It overwrites the current keymap if there is one. * builds the keyDescriptor string from the given parts * @param {boolean} hasCtrl Is Ctrl key enabled * @param {boolean} hasAlt Is Alt key enabled * @param {boolean} hasShift Is Shift key enabled * @param {string} key The key that's pressed * @return {string} The normalized key descriptor */ function _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key) { if (!key) { console.log("KeyBindingManager _buildKeyDescriptor() - No key provided!"); return ""; } var keyDescriptor = []; if (hasMacCtrl) { keyDescriptor.push("Ctrl"); } if (hasAlt) { keyDescriptor.push("Alt"); } if (hasShift) { keyDescriptor.push("Shift"); } if (hasCtrl) { // Windows display Ctrl first, Mac displays Command symbol last if (brackets.platform === "mac") { keyDescriptor.push("Cmd"); } else { keyDescriptor.unshift("Ctrl"); } } keyDescriptor.push(key); return keyDescriptor.join("-"); } /** * normalizes the incoming key descriptor so the modifier keys are always specified in the correct order * @param {string} The string for a key descriptor, can be in any order, the result will be Ctrl-Alt-Shift-<Key> * @return {string} The normalized key descriptor or null if the descriptor invalid */ function normalizeKeyDescriptorString(origDescriptor) { var hasMacCtrl = false, hasCtrl = false, hasAlt = false, hasShift = false, key = "", error = false; function _compareModifierString(left, right, previouslyFound, origDescriptor) { if (!left || !right) { return false; } left = left.trim().toLowerCase(); right = right.trim().toLowerCase(); var matched = (left.length > 0 && left === right); if (matched && previouslyFound) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Modifier defined twice: " + origDescriptor); } return matched; } origDescriptor.split("-").forEach(function parseDescriptor(ele, i, arr) { if (_compareModifierString("ctrl", ele, hasCtrl)) { if (brackets.platform === "mac") { hasMacCtrl = true; } else { hasCtrl = true; } } else if (_compareModifierString("cmd", ele, hasCtrl, origDescriptor)) { hasCtrl = true; } else if (_compareModifierString("alt", ele, hasAlt, origDescriptor)) { hasAlt = true; } else if (_compareModifierString("opt", ele, hasAlt, origDescriptor)) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Opt getting mapped to Alt from: " + origDescriptor); hasAlt = true; } else if (_compareModifierString("shift", ele, hasShift, origDescriptor)) { hasShift = true; } else if (key.length > 0) { console.log("KeyBindingManager normalizeKeyDescriptorString() - Multiple keys defined. Using key: " + key + " from: " + origDescriptor); error = true; } else { key = ele; } }); if (error) { return null; } // Check to see if the binding is for "-". if (key === "" && origDescriptor.search(/^.+--$/) !== -1) { key = "-"; } // '+' char is valid if it's the only key. Keyboard shortcut strings should use // unicode characters (unescaped). Keyboard shortcut display strings may use // unicode escape sequences (e.g. \u20AC euro sign) if ((key.indexOf("+")) >= 0 && (key.length > 1)) { return null; } return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key); } /** * @private * Looks for keycodes that have os-inconsistent keys and fixes them. * @param {number} The keycode from the keyboard event. * @param {string} The current best guess at what the key is. * @return {string} If the key is OS-inconsistent, the correct key; otherwise, the original key. **/ function _mapKeycodeToKey(keycode, key) { switch (keycode) { case 186: return ";"; case 187: return "="; case 188: return ","; case 189: return "-"; case 190: return "."; case 191: return "/"; case 192: return "`"; case 219: return "["; case 220: return "\\"; case 221: return "]"; case 222: return "'"; default: return key; } } /** * Takes a keyboard event and translates it into a key in a key map */ function _translateKeyboardEvent(event) { var hasMacCtrl = (brackets.platform === "win") ? false : (event.ctrlKey), hasCtrl = (brackets.platform === "win") ? (event.ctrlKey) : (event.metaKey), hasAlt = (event.altKey), hasShift = (event.shiftKey), key = String.fromCharCode(event.keyCode); //From the W3C, if we can get the KeyboardEvent.keyIdentifier then look here //As that will let us use keys like then function keys "F5" for commands. The //full set of values we can use is here //http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set var ident = event.keyIdentifier; if (ident) { if (ident.charAt(0) === "U" && ident.charAt(1) === "+") { //This is a unicode code point like "U+002A", get the 002A and use that key = String.fromCharCode(parseInt(ident.substring(2), 16)); } else { //This is some non-character key, just use the raw identifier key = ident; } } // Translate some keys to their common names if (key === "\t") { key = "Tab"; } else if (key === " ") { key = "Space"; } else { key = _mapKeycodeToKey(event.keyCode, key); } return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key); } /** * Convert normalized key representation to display appropriate for platform. * @param {!string} descriptor Normalized key descriptor. * @return {!string} Display/Operating system appropriate string */ function formatKeyDescriptor(descriptor) { var displayStr; if (brackets.platform === "mac") { displayStr = descriptor.replace(/-/g, ""); // remove dashes displayStr = displayStr.replace("Ctrl", "\u2303"); // Ctrl > control symbol displayStr = displayStr.replace("Cmd", "\u2318"); // Cmd > command symbol displayStr = displayStr.replace("Shift", "\u21E7"); // Shift > shift symbol displayStr = displayStr.replace("Alt", "\u2325"); // Alt > option symbol } else { displayStr = descriptor.replace(/-/g, "+"); } return displayStr; } /** * @private * @param {string} A normalized key-description string. * @return {boolean} true if the key is already assigned, false otherwise. */ function _isKeyAssigned(key) { return (_keyMap[key] !== undefined); } /** * @private * * @param {string} commandID * @param {string|{{key: string, displayKey: string}}} keyBinding - a single shortcut. * @param {?string} platform - undefined indicates all platforms * @return {?{key: string, displayKey:String}} Returns a record for valid key bindings */ function _addBinding(commandID, keyBinding, platform) { var key, result = null, normalized, normalizedDisplay, explicitPlatform = keyBinding.platform || platform, targetPlatform = explicitPlatform || brackets.platform, command; // skip if this binding doesn't match the current platform if (targetPlatform !== brackets.platform) { return null; } key = (keyBinding.key) || keyBinding; if (brackets.platform === "mac" && explicitPlatform === undefined) { key = key.replace("Ctrl", "Cmd"); if (keyBinding.displayKey !== undefined) { keyBinding.displayKey = keyBinding.displayKey.replace("Ctrl", "Cmd"); } } normalized = normalizeKeyDescriptorString(key); // skip if the key binding is invalid if (!normalized) { console.log("Failed to normalize " + key); return null; } // skip if the key is already assigned if (_isKeyAssigned(normalized)) { console.log("Cannot assign " + normalized + " to " + commandID + ". It is already assigned to " + _keyMap[normalized]); return null; } // optional display-friendly string (e.g. CMD-+ instead of CMD-=) normalizedDisplay = (keyBinding.displayKey) ? normalizeKeyDescriptorString(keyBinding.displayKey) : normalized; // 1-to-many commandID mapping to key binding if (!_commandMap[commandID]) { _commandMap[commandID] = []; } result = {key: normalized, displayKey: normalizedDisplay}; _commandMap[commandID].push(result); // 1-to-1 key binding to commandID _keyMap[normalized] = {commandID: commandID, key: normalized, displayKey: normalizedDisplay}; // notify listeners command = CommandManager.get(commandID); if (command) { $(command).triggerHandler("keyBindingAdded", [result]); } return result; } /** * Returns a copy of the keymap * @returns {!Object.<string, {commandID: string, key: string, displayKey: string}>} */ function getKeymap() { return $.extend({}, _keyMap); } /** * Process the keybinding for the current key. * * @param {string} A key-description string. * @return {boolean} true if the key was processed, false otherwise */ function handleKey(key) { if (_enabled && _keyMap[key]) { CommandManager.execute(_keyMap[key].commandID); return true; } return false; } // TODO (issue #414): Replace this temporary fix with a more robust solution to handle focus and modality /** * Enable or disable key bindings. Clients such as dialogs may wish to disable * global key bindings temporarily. * * @param {string} A key-description string. * @return {boolean} true if the key was processed, false otherwise */ function setEnabled(value) { _enabled = value; } /** * Add one or more key bindings to a particular Command. * * @param {!string} commandID * @param {?({key: string, displayKey: string} | Array.<{key: string, displayKey: string, platform: string)}>} keyBindings - a single key binding * or an array of keybindings. Example: "Shift-Cmd-F". Mac and Win key equivalents are automatically * mapped to each other. Use displayKey property to display a different string (e.g. "CMD+" instead of "CMD="). * @param {?string} platform - the target OS of the keyBindings either "mac" or "win". If undefined, all platforms will use * the key binding. Ignored if keyBindings is passed an Array. * @return {{key: string, displayKey:String}|Array.<{key: string, displayKey:String}>} Returns record(s) for valid key binding(s) */ function addBinding(commandID, keyBindings, platform) { if ((commandID === null) || (commandID === undefined) || !keyBindings) { return; } var normalizedBindings = [], targetPlatform, results; if (Array.isArray(keyBindings)) { var keyBinding; results = []; keyBindings.forEach(function (keyBindingRequest) { keyBinding = _addBinding(commandID, keyBindingRequest, keyBindingRequest.platform); if (keyBinding) { results.push(keyBinding); } }); } else { results = _addBinding(commandID, keyBindings, platform); } return results; } /** * Remove a key binding from _keymap * * @param {!string} key - a key-description string that may or may not be normalized. * @param {?string} platform - OS from which to remove the binding (all platforms if unspecified) */ function removeBinding(key, platform) { if (!key || ((platform !== null) && (platform !== undefined) && (platform !== brackets.platform))) { return; } var normalizedKey = normalizeKeyDescriptorString(key); if (!normalizedKey) { console.log("Fail to nomalize " + key); } else if (_isKeyAssigned(normalizedKey)) { var binding = _keyMap[normalizedKey], command = CommandManager.get(binding.commandID), bindings = _commandMap[binding.commandID]; // delete key binding record delete _keyMap[normalizedKey]; if (bindings) { // delete mapping from command to key binding _commandMap[binding.commandID] = bindings.filter(function (b) { return (b.key !== normalizedKey); }); if (command) { $(command).triggerHandler("keyBindingRemoved", [{key: normalizedKey, displayKey: binding.displayKey}]); } } } } /** * Retrieve key bindings currently associated with a command * * @param {!string} command - A command ID * @return {!Array.<{{key: string, displayKey: string}}>} An array of associated key bindings. */ function getKeyBindings(commandID) { var bindings = _commandMap[commandID]; return bindings || []; } /** * Install keydown event listener. */ function init() { // init window.document.body.addEventListener( "keydown", function (event) { if (handleKey(_translateKeyboardEvent(event))) { event.stopPropagation(); } }, true ); } // unit test only exports._reset = _reset; // Define public API exports.init = init; exports.getKeymap = getKeymap; exports.handleKey = handleKey; exports.setEnabled = setEnabled; exports.addBinding = addBinding; exports.removeBinding = removeBinding; exports.formatKeyDescriptor = formatKeyDescriptor; exports.getKeyBindings = getKeyBindings; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window */ /** * Utilities for creating and managing standard modal dialogs. */ define('widgets/Dialogs',['require','exports','module','command/KeyBindingManager'],function (require, exports, module) { var KeyBindingManager = require("command/KeyBindingManager"); var DIALOG_BTN_CANCEL = "cancel", DIALOG_BTN_OK = "ok", DIALOG_BTN_DONTSAVE = "dontsave", DIALOG_CANCELED = "_canceled", DIALOG_BTN_DOWNLOAD = "download"; // TODO: (issue #258) In future, we should templatize the HTML for the dialogs rather than having // it live directly in the HTML. var DIALOG_ID_ERROR = "error-dialog", DIALOG_ID_SAVE_CLOSE = "save-close-dialog", DIALOG_ID_EXT_CHANGED = "ext-changed-dialog", DIALOG_ID_EXT_DELETED = "ext-deleted-dialog", DIALOG_ID_LIVE_DEVELOPMENT = "live-development-error-dialog", DIALOG_ID_ABOUT = "about-dialog", DIALOG_ID_UPDATE = "update-dialog"; function _dismissDialog(dlg, buttonId) { dlg.data("buttonId", buttonId); dlg.modal(true).hide(); } function _hasButton(dlg, buttonId) { return dlg.find("[data-button-id='" + buttonId + "']"); } var _handleKeyDown = function (e) { var primaryBtn = this.find(".primary"), buttonId = null, which = String.fromCharCode(e.which); if (e.which === 13) { // Click primary button if (primaryBtn) { buttonId = primaryBtn.attr("data-button-id"); } } else if (e.which === 32) { // Space bar on focused button this.find(".dialog-button:focus").click(); } else if (brackets.platform === "mac") { // CMD+D Don't Save if (e.metaKey && (which === "D")) { if (_hasButton(this, DIALOG_BTN_DONTSAVE)) { buttonId = DIALOG_BTN_DONTSAVE; } // FIXME (issue #418) CMD+. Cancel swallowed by native shell } else if (e.metaKey && (e.which === 190)) { buttonId = DIALOG_BTN_CANCEL; } } else { // if (brackets.platform === "win") { // 'N' Don't Save if (which === "N") { if (_hasButton(this, DIALOG_BTN_DONTSAVE)) { buttonId = DIALOG_BTN_DONTSAVE; } } } if (buttonId) { _dismissDialog(this, buttonId); } else if (!($.contains(this.get(0), e.target)) || ($(e.target).filter(":input").length === 0)) { // Stop the event if the target is not inside the dialog // or if the target is not a form element. // TODO (issue #414): more robust handling of dialog scoped // vs. global key bindings e.stopPropagation(); e.preventDefault(); } }; /** * General purpose modal dialog. Assumes that: * -- the root tag of the dialog is marked with a unique class name (passed as dlgClass), as well as the * classes "template modal hide". * -- the HTML for the dialog contains elements with "title" and "message" classes, as well as a number * of elements with "dialog-button" class, each of which has a "data-button-id". * * @param {string} dlgClass The class of the dialog node in the HTML. * @param {string=} title The title of the error dialog. Can contain HTML markup. If unspecified, title in * the HTML template is used unchanged. * @param {string=} message The message to display in the error dialog. Can contain HTML markup. If * unspecified, body in the HTML template is used unchanged. * @return {$.Promise} a promise that will be resolved with the ID of the clicked button when the dialog * is dismissed. Never rejected. */ function showModalDialog(dlgClass, title, message) { var result = $.Deferred(), promise = result.promise(); // We clone the HTML rather than using it directly so that if two dialogs of the same // type happen to show up, they can appear at the same time. (This is an edge case that // shouldn't happen often, but we can't prevent it from happening since everything is // asynchronous.) var $dlg = $("." + dlgClass + ".template") .clone() .removeClass("template") .addClass("instance") .appendTo(window.document.body); if ($dlg.length === 0) { throw new Error("Dialog id " + dlgClass + " does not exist"); } // Save the dialog promise for unit tests $dlg.data("promise", promise); // Set title and message if (title) { $(".dialog-title", $dlg).html(title); } if (message) { $(".dialog-message", $dlg).html(message); } var handleKeyDown = _handleKeyDown.bind($dlg); // Pipe dialog-closing notification back to client code $dlg.one("hidden", function () { var buttonId = $dlg.data("buttonId"); if (!buttonId) { // buttonId will be undefined if closed via Bootstrap's "x" button buttonId = DIALOG_BTN_CANCEL; } // Let call stack return before notifying that dialog has closed; this avoids issue #191 // if the handler we're triggering might show another dialog (as long as there's no // fade-out animation) window.setTimeout(function () { result.resolve(buttonId); }, 0); // Remove the dialog instance from the DOM. $dlg.remove(); // Remove keydown event handler window.document.body.removeEventListener("keydown", handleKeyDown, true); KeyBindingManager.setEnabled(true); }).one("shown", function () { // Set focus to the default button var primaryBtn = $dlg.find(".primary"); if (primaryBtn) { primaryBtn.focus(); } // Listen for dialog keyboard shortcuts window.document.body.addEventListener("keydown", handleKeyDown, true); KeyBindingManager.setEnabled(false); }); // Click handler for buttons $dlg.one("click", ".dialog-button", function (e) { _dismissDialog($dlg, $(this).attr("data-button-id")); }); // Run the dialog $dlg.modal({ backdrop: "static", show: true, keyboard: true }); return promise; } /** * Immediately closes any dialog instances with the given class. The dialog callback for each instance will * be called with the special buttonId DIALOG_CANCELED (note: callback is run asynchronously). */ function cancelModalDialogIfOpen(dlgClass) { $("." + dlgClass + ".instance").each(function (index, dlg) { if ($(dlg).is(":visible")) { // Bootstrap breaks if try to hide dialog that's already hidden _dismissDialog($(dlg), DIALOG_CANCELED); } }); } exports.DIALOG_BTN_CANCEL = DIALOG_BTN_CANCEL; exports.DIALOG_BTN_OK = DIALOG_BTN_OK; exports.DIALOG_BTN_DONTSAVE = DIALOG_BTN_DONTSAVE; exports.DIALOG_CANCELED = DIALOG_CANCELED; exports.DIALOG_BTN_DOWNLOAD = DIALOG_BTN_DOWNLOAD; exports.DIALOG_ID_ERROR = DIALOG_ID_ERROR; exports.DIALOG_ID_SAVE_CLOSE = DIALOG_ID_SAVE_CLOSE; exports.DIALOG_ID_EXT_CHANGED = DIALOG_ID_EXT_CHANGED; exports.DIALOG_ID_EXT_DELETED = DIALOG_ID_EXT_DELETED; exports.DIALOG_ID_LIVE_DEVELOPMENT = DIALOG_ID_LIVE_DEVELOPMENT; exports.DIALOG_ID_ABOUT = DIALOG_ID_ABOUT; exports.DIALOG_ID_UPDATE = DIALOG_ID_UPDATE; exports.showModalDialog = showModalDialog; exports.cancelModalDialogIfOpen = cancelModalDialogIfOpen; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, brackets, $, window */ /** * This is a collection of utility functions for gathering performance data. */ define('utils/PerfUtils',['require','exports','module'],function (require, exports, module) { /** * Flag to enable/disable performance data gathering. Default is true (enabled) * @type {boolean} enabled */ var enabled = true; /** * Peformance data is stored in this hash object. The key is the name of the * test (passed to markStart/addMeasurement), and the value is the time, in * milliseconds, that it took to run the test. If multiple runs of the same test * are made, the value is an Array with each run stored as an entry in the Array. */ var perfData = {}; /** * Active tests. This is a hash of all tests that have had markStart() called, * but have not yet had addMeasurement() called. */ var activeTests = {}; /** * Updatable tests. This is a hash of all tests that have had markStart() called, * and have had updateMeasurement() called. Caller must explicitly remove tests * from this list using finalizeMeasurement() */ var updatableTests = {}; /** * Hash of measurement IDs */ var perfMeasurementIds = {}; /** * @private * A unique key to log performance data * * @param {!string} id Unique ID for this measurement name * @param {!name} name A short name for this measurement */ function PerfMeasurement(id, name) { this.id = id; this.name = name; } /** * Create a new PerfMeasurement key. Adds itself to the module export. * Can be accessed on the module, e.g. PerfUtils.MY_PERF_KEY. * * @param {!string} id Unique ID for this measurement name * @param {!name} name A short name for this measurement */ function createPerfMeasurement(id, name) { if (perfMeasurementIds[id]) { throw new Error("Performance measurement " + id + " is already defined"); } var pm = new PerfMeasurement(id, name); exports[id] = pm; return pm; } /** * @private * Convert a PerfMeasurement instance to it's id. Otherwise uses the string name for backwards compatibility. */ function toMeasurementId(o) { return (o instanceof PerfMeasurement) ? o.id : o; } /** * @private * Helper function for markStart() * * @param {string} name Timer name. * @param {number} time Timer start time. */ function _markStart(name, time) { if (activeTests[name]) { throw new Error("Recursive tests with the same name are not supported. Timer name: " + name); } activeTests[name] = { startTime: time }; } /** * Start a new named timer. The name should be as descriptive as possible, since * this name will appear as an entry in the performance report. * For example: "Open file: /Users/brackets/src/ProjectManager.js" * * Multiple timers can be opened simultaneously, but all open timers must have * a unique name. * * @param {(string|Array.<string>)} name Single name or an Array of names. * @returns {string} timer name. Returned for convenience to store and use * for calling addMeasure(). Since name is often creating via concatenating * strings this return value allows clients to construct the name once. */ function markStart(name) { if (!enabled) { return; } var time = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); // Array of names can be passed in to have multiple timers with same start time if (Array.isArray(name)) { var i; for (i = 0; i < name.length; i++) { _markStart(name[i], time); } } else { _markStart(name, time); } return name; } /** * Stop a timer and add its measurements to the performance data. * * Multiple measurements can be stored for any given name. If there are * multiple values for a name, they are stored in an Array. * * If markStart() was not called for the specified timer, the * measured time is relative to app startup. * * @param {string} name Timer name. */ function addMeasurement(name) { if (!enabled) { return; } var elapsedTime = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); if (activeTests[name]) { elapsedTime -= activeTests[name].startTime; delete activeTests[name]; } if (perfData[name]) { // We have existing data, add to it if (Array.isArray(perfData[name])) { perfData[name].push(elapsedTime); } else { // Current data is a number, convert to Array perfData[name] = [perfData[name], elapsedTime]; } } else { perfData[name] = elapsedTime; } // Real time logging //console.log(name + " " + elapsedTime); } /** * This function is similar to addMeasurement(), but it allows timing the * *last* event, when you don't know which event will be the last one. * * Tests that are in the activeTests list, have not yet been added, so add * measurements to the performance data, and move test to updatableTests list. * A test is moved to the updatable list so that it no longer passes isActive(). * * Tests that are already in the updatableTests list are updated. * * Caller must explicitly remove test from the updatableTests list using * finalizeMeasurement(). * * If markStart() was not called for the specified timer, there is no way to * determine if this is the first or subsequent call, so the measurement is * not updatable, and it is handled in addMeasurement(). * * @param {string} name Timer name. */ function updateMeasurement(name) { var elapsedTime = brackets.app.getElapsedMilliseconds(); name = toMeasurementId(name); if (updatableTests[name]) { // update existing measurement elapsedTime -= updatableTests[name].startTime; // update if (perfData[name] && Array.isArray(perfData[name])) { // We have existing data and it's an array, so update the last entry perfData[name][perfData[name].length - 1] = elapsedTime; } else { // No current data or a single entry, so set/update it perfData[name] = elapsedTime; } } else { // not yet in updatable list if (activeTests[name]) { // save startTime in updatable list before addMeasurement() deletes it updatableTests[name] = { startTime: activeTests[name].startTime }; } // let addMeasurement() handle the initial case addMeasurement(name); } } /** * Remove timer from lists so next action starts a new measurement * * updateMeasurement may not have been called, so timer may be * in either or neither list, but should never be in both. * * @param {string} name Timer name. */ function finalizeMeasurement(name) { name = toMeasurementId(name); if (activeTests[name]) { delete activeTests[name]; } if (updatableTests[name]) { delete updatableTests[name]; } } /** * Returns whether a timer is active or not, where "active" means that * timer has been started with addMark(), but has not been added to perfdata * with addMeasurement(). * * @param {string} name Timer name. * @return {boolean} Whether a timer is active or not. */ function isActive(name) { return (activeTests[name]) ? true : false; } /** * Returns the performance data as a tab deliminted string * @returns {string} */ function getDelimitedPerfData() { var getValue = function (entry) { // return single value, or tab deliminted values for an array if (Array.isArray(entry)) { var i, values = ""; for (i = 0; i < entry.length; i++) { values += entry[i]; if (i < entry.length - 1) { values += ", "; } } return values; } else { return entry; } }; var testName, index, result = ""; $.each(perfData, function (testName, entry) { result += getValue(entry) + "\t" + testName + "\n"; }); return result; } /** * Returns the measured value for the given measurement name. * @param {string|PerfMeasurement} name The measurement to retreive. */ function getData(name) { if (!name) { return perfData; } return perfData[toMeasurementId(name)]; } function searchData(regExp) { var keys = Object.keys(perfData).filter(function (key) { return regExp.test(key); }); var datas = []; keys.forEach(function (key) { datas.push(perfData[key]); }); return datas; } /** * Clear all logs including metric data and active tests. */ function clear() { perfData = {}; activeTests = {}; updatableTests = {}; } // create performance measurement constants createPerfMeasurement("INLINE_EDITOR_OPEN", "Open inline editor"); createPerfMeasurement("INLINE_EDITOR_CLOSE", "Close inline editor"); // extensions may create additional measurement constants during their lifecycle exports.addMeasurement = addMeasurement; exports.finalizeMeasurement = finalizeMeasurement; exports.isActive = isActive; exports.markStart = markStart; exports.getData = getData; exports.searchData = searchData; exports.updateMeasurement = updateMeasurement; exports.getDelimitedPerfData = getDelimitedPerfData; exports.createPerfMeasurement = createPerfMeasurement; exports.clear = clear; }); /** * @license RequireJS i18n 1.0.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/requirejs for details */ /*jslint regexp: false, nomen: false, plusplus: false, strict: false */ /*global require: false, navigator: false, define: false */ /** * This plugin handles i18n! prefixed modules. It does the following: * * 1) A regular module can have a dependency on an i18n bundle, but the regular * module does not want to specify what locale to load. So it just specifies * the top-level bundle, like "i18n!nls/colors". * * This plugin will load the i18n bundle at nls/colors, see that it is a root/master * bundle since it does not have a locale in its name. It will then try to find * the best match locale available in that master bundle, then request all the * locale pieces for that best match locale. For instance, if the locale is "en-us", * then the plugin will ask for the "en-us", "en" and "root" bundles to be loaded * (but only if they are specified on the master bundle). * * Once all the bundles for the locale pieces load, then it mixes in all those * locale pieces into each other, then finally sets the context.defined value * for the nls/colors bundle to be that mixed in locale. * * 2) A regular module specifies a specific locale to load. For instance, * i18n!nls/fr-fr/colors. In this case, the plugin needs to load the master bundle * first, at nls/colors, then figure out what the best match locale is for fr-fr, * since maybe only fr or just root is defined for that locale. Once that best * fit is found, all of its locale pieces need to have their bundles loaded. * * Once all the bundles for the locale pieces load, then it mixes in all those * locale pieces into each other, then finally sets the context.defined value * for the nls/fr-fr/colors bundle to be that mixed in locale. */ (function () { //regexp for reconstructing the master bundle name from parts of the regexp match //nlsRegExp.exec("foo/bar/baz/nls/en-ca/foo") gives: //["foo/bar/baz/nls/en-ca/foo", "foo/bar/baz/nls/", "/", "/", "en-ca", "foo"] //nlsRegExp.exec("foo/bar/baz/nls/foo") gives: //["foo/bar/baz/nls/foo", "foo/bar/baz/nls/", "/", "/", "foo", ""] //so, if match[5] is blank, it means this is the top bundle definition. var nlsRegExp = /(^.*(^|\/)nls(\/|$))([^\/]*)\/?([^\/]*)/, empty = {}; //Helper function to avoid repeating code. Lots of arguments in the //desire to stay functional and support RequireJS contexts without having //to know about the RequireJS contexts. function addPart(locale, master, needed, toLoad, prefix, suffix) { if (master[locale]) { needed.push(locale); if (master[locale] === true || master[locale] === 1) { toLoad.push(prefix + locale + '/' + suffix); } } } function addIfExists(req, locale, toLoad, prefix, suffix) { var fullName = prefix + locale + '/' + suffix; if (require._fileExists(req.toUrl(fullName))) { toLoad.push(fullName); } } /** * Simple function to mix in properties from source into target, * but only if target does not already have a property of the same name. * This is not robust in IE for transferring methods that match * Object.prototype names, but the uses of mixin here seem unlikely to * trigger a problem related to that. */ function mixin(target, source, force) { for (var prop in source) { if (!(prop in empty) && (!(prop in target) || force)) { target[prop] = source[prop]; } } } define('i18n',{ version: '1.0.0', /** * Called when a dependency needs to be loaded. */ load: function (name, req, onLoad, config) { config = config || {}; var masterName, match = nlsRegExp.exec(name), prefix = match[1], locale = match[4], suffix = match[5], parts = locale.split("-"), toLoad = [], value = {}, i, part, current = ""; //If match[5] is blank, it means this is the top bundle definition, //so it does not have to be handled. Locale-specific requests //will have a match[4] value but no match[5] if (match[5]) { //locale-specific bundle prefix = match[1]; masterName = prefix + suffix; } else { //Top-level bundle. masterName = name; suffix = match[4]; locale = config.locale || (config.locale = typeof navigator === "undefined" ? "root" : (navigator.language || navigator.userLanguage || "root").toLowerCase()); parts = locale.split("-"); } if (config.isBuild) { //Check for existence of all locale possible files and //require them if exist. toLoad.push(masterName); addIfExists(req, "root", toLoad, prefix, suffix); for (i = 0; (part = parts[i]); i++) { current += (current ? "-" : "") + part; addIfExists(req, current, toLoad, prefix, suffix); } req(toLoad, function () { onLoad(); }); } else { //First, fetch the master bundle, it knows what locales are available. req([masterName], function (master) { //Figure out the best fit var needed = []; //Always allow for root, then do the rest of the locale parts. addPart("root", master, needed, toLoad, prefix, suffix); for (i = 0; (part = parts[i]); i++) { current += (current ? "-" : "") + part; addPart(current, master, needed, toLoad, prefix, suffix); } //Load all the parts missing. req(toLoad, function () { var i, partBundle; for (i = needed.length - 1; i > -1 && (part = needed[i]); i--) { partBundle = master[part]; if (partBundle === true || partBundle === 1) { partBundle = req(prefix + part + '/' + suffix); } mixin(value, partBundle); } //All done, notify the loader. onLoad(value); }); }); } } }); }()); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('nls/strings',['require','exports','module'],function (require, exports, module) { // Code that needs to display user strings should call require("strings") to load // src/strings.js. This file will dynamically load strings.js for the specified brackets.locale. // // See the README.md file in this folder for information on how to add a new translation for // another language or locale. // // TODO: dynamically populate the local prefix list below? module.exports = { root: true, "de": true, "fr": true }; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ define('nls/root/strings',{ /** * Errors */ // General file io error strings "GENERIC_ERROR" : "(error {0})", "NOT_FOUND_ERR" : "The file could not be found.", "NOT_READABLE_ERR" : "The file could not be read.", "NO_MODIFICATION_ALLOWED_ERR" : "The target directory cannot be modified.", "NO_MODIFICATION_ALLOWED_ERR_FILE" : "The permissions do not allow you to make modifications.", // Project error strings "ERROR_LOADING_PROJECT" : "Error loading project", "OPEN_DIALOG_ERROR" : "An error occurred when showing the open file dialog. (error {0})", "REQUEST_NATIVE_FILE_SYSTEM_ERROR" : "An error occurred when trying to load the directory <span class='dialog-filename'>{0}</span>. (error {1})", "READ_DIRECTORY_ENTRIES_ERROR" : "An error occurred when reading the contents of the directory <span class='dialog-filename'>{0}</span>. (error {1})", // File open/save error string "ERROR_OPENING_FILE_TITLE" : "Error opening file", "ERROR_OPENING_FILE" : "An error occurred when trying to open the file <span class='dialog-filename'>{0}</span>. {1}", "ERROR_RELOADING_FILE_TITLE" : "Error reloading changes from disk", "ERROR_RELOADING_FILE" : "An error occurred when trying to reload the file <span class='dialog-filename'>{0}</span>. {1}", "ERROR_SAVING_FILE_TITLE" : "Error saving file", "ERROR_SAVING_FILE" : "An error occurred when trying to save the file <span class='dialog-filename'>{0}</span>. {1}", "INVALID_FILENAME_TITLE" : "Invalid file name", "INVALID_FILENAME_MESSAGE" : "Filenames cannot contain the following characters: /?*:;{}<>\\|", "FILE_ALREADY_EXISTS" : "The file <span class='dialog-filename'>{0}</span> already exists.", "ERROR_CREATING_FILE_TITLE" : "Error creating file", "ERROR_CREATING_FILE" : "An error occurred when trying to create the file <span class='dialog-filename'>{0}</span>. {1}", // Application error strings "ERROR_BRACKETS_IN_BROWSER_TITLE" : "Oops! Brackets doesn't run in browsers yet.", "ERROR_BRACKETS_IN_BROWSER" : "Brackets is built in HTML, but right now it runs as a desktop app so you can use it to edit local files. Please use the application shell in the <b>github.com/adobe/brackets-app</b> repo to run Brackets.", // FileIndexManager error string "ERROR_MAX_FILES_TITLE" : "Error Indexing Files", "ERROR_MAX_FILES" : "The maximum number of files have been indexed. Actions that look up files in the index may function incorrectly.", // CSSManager error strings "ERROR_PARSE_TITLE" : "Error parsing CSS file(s):", // Live Development error strings "ERROR_LAUNCHING_BROWSER_TITLE" : "Error launching browser", "ERROR_CANT_FIND_CHROME" : "The Google Chrome browser could not be found. Please make sure it is installed.", "ERROR_LAUNCHING_BROWSER" : "An error occurred when launching the browser. (error {0})", "LIVE_DEVELOPMENT_ERROR_TITLE" : "Live Development Error", "LIVE_DEVELOPMENT_ERROR_MESSAGE" : "A live development connection to Chrome could not be established. For live development to work, Chrome needs to be started with remote debugging enabled.<br /><br />Would you like to relaunch Chrome and enable remote debugging?", "LIVE_DEV_NEED_HTML_MESSAGE" : "Open an HTML file in order to launch live preview.", "LIVE_DEV_STATUS_TIP_NOT_CONNECTED" : "Live File Preview", "LIVE_DEV_STATUS_TIP_PROGRESS1" : "Live File Preview: Connecting...", "LIVE_DEV_STATUS_TIP_PROGRESS2" : "Live File Preview: Initializing...", "LIVE_DEV_STATUS_TIP_CONNECTED" : "Disconnect Live File Preview", "SAVE_CLOSE_TITLE" : "Save Changes", "SAVE_CLOSE_MESSAGE" : "Do you want to save the changes you made in the document <span class='dialog-filename'>{0}</span>?", "SAVE_CLOSE_MULTI_MESSAGE" : "Do you want to save your changes to the following files?", "EXT_MODIFIED_TITLE" : "External Changes", "EXT_MODIFIED_MESSAGE" : "<span class='dialog-filename'>{0}</span> has been modified on disk, but also has unsaved changes in Brackets.<br /><br />Which version do you want to keep?", "EXT_DELETED_MESSAGE" : "<span class='dialog-filename'>{0}</span> has been deleted on disk, but has unsaved changes in Brackets.<br /><br />Do you want to keep your changes?", // Find, Replace, Find in Files "SEARCH_REGEXP_INFO" : "Use /re/ syntax for regexp search", "WITH" : "With", "BUTTON_YES" : "Yes", "BUTTON_NO" : "No", "BUTTON_STOP" : "Stop", "OPEN_FILE" : "Open File", "RELEASE_NOTES" : "Release Notes", "NO_UPDATE_TITLE" : "You're up to date!", "NO_UPDATE_MESSAGE" : "You are running the latest version of Brackets.", // Switch language "LANGUAGE_TITLE" : "Switch Language", "LANGUAGE_MESSAGE" : "Please select the desired language from the list below:", "LANGUAGE_SUBMIT" : "Reload Brackets", "LANGUAGE_CANCEL" : "Cancel", /** * ProjectManager */ "UNTITLED" : "Untitled", /** * Command Name Constants */ // File menu commands "FILE_MENU" : "File", "CMD_FILE_NEW" : "New", "CMD_FILE_OPEN" : "Open\u2026", "CMD_ADD_TO_WORKING_SET" : "Add To Working Set", "CMD_OPEN_FOLDER" : "Open Folder\u2026", "CMD_FILE_CLOSE" : "Close", "CMD_FILE_CLOSE_ALL" : "Close All", "CMD_FILE_SAVE" : "Save", "CMD_FILE_SAVE_ALL" : "Save All", "CMD_LIVE_FILE_PREVIEW" : "Live File Preview", "CMD_QUIT" : "Quit", // Edit menu commands "EDIT_MENU" : "Edit", "CMD_SELECT_ALL" : "Select All", "CMD_FIND" : "Find", "CMD_FIND_IN_FILES" : "Find in Files", "CMD_FIND_NEXT" : "Find Next", "CMD_FIND_PREVIOUS" : "Find Previous", "CMD_REPLACE" : "Replace", "CMD_INDENT" : "Indent", "CMD_UNINDENT" : "Unindent", "CMD_DUPLICATE" : "Duplicate", "CMD_COMMENT" : "Comment/Uncomment Lines", "CMD_LINE_UP" : "Move Line(s) Up", "CMD_LINE_DOWN" : "Move Line(s) Down", // View menu commands "VIEW_MENU" : "View", "CMD_HIDE_SIDEBAR" : "Hide Sidebar", "CMD_SHOW_SIDEBAR" : "Show Sidebar", "CMD_INCREASE_FONT_SIZE" : "Increase Font Size", "CMD_DECREASE_FONT_SIZE" : "Decrease Font Size", "CMD_RESTORE_FONT_SIZE" : "Restore Font Size", // Navigate menu Commands "NAVIGATE_MENU" : "Navigate", "CMD_QUICK_OPEN" : "Quick Open", "CMD_GOTO_LINE" : "Go to Line", "CMD_GOTO_DEFINITION" : "Go to Definition", "CMD_TOGGLE_QUICK_EDIT" : "Quick Edit", "CMD_QUICK_EDIT_PREV_MATCH" : "Previous Match", "CMD_QUICK_EDIT_NEXT_MATCH" : "Next Match", "CMD_NEXT_DOC" : "Next Document", "CMD_PREV_DOC" : "Previous Document", // Debug menu commands "DEBUG_MENU" : "Debug", "CMD_REFRESH_WINDOW" : "Reload Brackets", "CMD_SHOW_DEV_TOOLS" : "Show Developer Tools", "CMD_RUN_UNIT_TESTS" : "Run Tests", "CMD_JSLINT" : "Enable JSLint", "CMD_SHOW_PERF_DATA" : "Show Performance Data", "CMD_NEW_BRACKETS_WINDOW" : "New Brackets Window", "CMD_SHOW_EXTENSIONS_FOLDER" : "Show Extensions Folder", "CMD_USE_TAB_CHARS" : "Use Tab Characters", "CMD_SWITCH_LANGUAGE" : "Switch Language", "CMD_CHECK_FOR_UPDATE" : "Check for Updates", // Help menu commands "CMD_ABOUT" : "About", // Special commands invoked by the native shell "CMD_CLOSE_WINDOW" : "Close Window", "CMD_ABORT_QUIT" : "Abort Quit", // Strings for main-view.html "EXPERIMENTAL_BUILD" : "Experimental Build", "JSLINT_ERRORS" : "JSLint Errors", "SEARCH_RESULTS" : "Search Results", "OK" : "OK", "DONT_SAVE" : "Don't Save", "SAVE" : "Save", "CANCEL" : "Cancel", "RELOAD_FROM_DISK" : "Reload from Disk", "KEEP_CHANGES_IN_EDITOR" : "Keep Changes in Editor", "CLOSE_DONT_SAVE" : "Close (Don't Save)", "RELAUNCH_CHROME" : "Relaunch Chrome", "ABOUT" : "About", "BRACKETS" : "Brackets", "CLOSE" : "Close", "ABOUT_TEXT_LINE1" : "sprint 13 experimental build ", "ABOUT_TEXT_LINE2" : "Copyright 2012 Adobe Systems Incorporated and its licensors. All rights reserved.", "ABOUT_TEXT_LINE3" : "Notices; terms and conditions pertaining to third party software are located at ", "ABOUT_TEXT_LINE4" : " and incorporated by reference herein.", "ABOUT_TEXT_LINE5" : "Documentation and source at ", "UPDATE_NOTIFICATION_TOOLTIP" : "There's a new build of Brackets available! Click here for details.", "UPDATE_AVAILABLE_TITLE" : "Update Available", "UPDATE_MESSAGE" : "Hey, there's a new build of Brackets available. Here are some of the new features:", "GET_IT_NOW" : "Get it now!" }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define */ /** * This file provides the interface to user visible strings in Brackets. Code that needs * to display strings should should load this module by calling var Strings = require("strings"). * The i18n plugin will dynamically load the strings for the right locale and populate * the exports variable. See src\nls\strings.js for the master file of English strings. */ define('strings',['require','exports','module','i18n!nls/strings'],function (require, exports, module) { module.exports = require("i18n!nls/strings"); }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $ */ /** * Utilities functions related to string manipulation * */ define('utils/StringUtils',['require','exports','module'],function (require, exports, module) { /** * Format a string by replacing placeholder symbols with passed in arguments. * * Example: var formatted = StringUtils.format("Hello {0}", "World"); * * @param {string} str The base string * @param {...} Arguments to be substituted into the string * * @return {string} Formatted string */ function format(str) { // arguments[0] is the base string, so we need to adjust index values here var args = [].slice.call(arguments, 1); return str.replace(/\{(\d+)\}/g, function (match, num) { return typeof args[num] !== "undefined" ? args[num] : match; }); } function htmlEscape(str) { return String(str) .replace(/&/g, "&") .replace(/"/g, """) .replace(/'/g, "'") .replace(/</g, "<") .replace(/>/g, ">"); } function regexEscape(str) { return str.replace(/([.?*+\^$\[\]\\(){}|\-])/g, "\\$1"); } // Periods (aka "dots") are allowed in HTML identifiers, but jQuery interprets // them as the start of a class selector, so they need to be escaped function jQueryIdEscape(str) { return str.replace(/\./g, "\\."); } /** * Splits the text by new line characters and returns an array of lines * @param {string} text * @return {Array.<string>} lines */ function getLines(text) { return text.split("\n"); } /** * Returns a line number corresponding to an offset in some text. The text can * be specified as a single string or as an array of strings that correspond to * the lines of the string. * * Specify the text in lines when repeatedly calling the function on the same * text in a loop. Use getLines() to divide the text into lines, then repeatedly call * this function to compute a line number from the offset. * * @param {string | Array.<string>} textOrLines - string or array of lines from which * to compute the line number from the offset * @param {number} offset * @return {number} line number */ function offsetToLineNum(textOrLines, offset) { if (Array.isArray(textOrLines)) { var lines = textOrLines, total = 0, line; for (line = 0; line < lines.length; line++) { if (total < offset) { // add 1 per line since /n were removed by splitting, but they needed to // contribute to the total offset count total += lines[line].length + 1; } else if (total === offset) { return line; } else { return line - 1; } } // if offset is NOT over the total then offset is in the last line if (offset <= total) { return line - 1; } else { return undefined; } } else { return textOrLines.substr(0, offset).split("\n").length - 1; } } // Define public API exports.format = format; exports.htmlEscape = htmlEscape; exports.regexEscape = regexEscape; exports.jQueryIdEscape = jQueryIdEscape; exports.getLines = getLines; exports.offsetToLineNum = offsetToLineNum; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global define, $, FileError, brackets, unescape, window */ /** * Set of utilites for working with files and text content. */ define('file/FileUtils',['require','exports','module','file/NativeFileSystem','utils/PerfUtils','widgets/Dialogs','strings','utils/StringUtils'],function (require, exports, module) { var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PerfUtils = require("utils/PerfUtils"), Dialogs = require("widgets/Dialogs"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), Encodings = NativeFileSystem.Encodings; /** * Asynchronously reads a file as UTF-8 encoded text. * @return {$.Promise} a jQuery promise that will be resolved with the * file's text content plus its timestamp, or rejected with a FileError if * the file can not be read. */ function readAsText(fileEntry) { var result = new $.Deferred(), reader; // Measure performance var perfTimerName = PerfUtils.markStart("readAsText:\t" + fileEntry.fullPath); result.always(function () { PerfUtils.addMeasurement(perfTimerName); }); // Read file reader = new NativeFileSystem.FileReader(); fileEntry.file(function (file) { reader.onload = function (event) { var text = event.target.result; fileEntry.getMetadata( function (metadata) { result.resolve(text, metadata.modificationTime); }, function (error) { result.reject(error); } ); }; reader.onerror = function (event) { result.reject(event.target.error); }; reader.readAsText(file, Encodings.UTF8); }); return result.promise(); } /** * Asynchronously writes a file as UTF-8 encoded text. * @param {!FileEntry} fileEntry * @param {!string} text * @return {$.Promise} a jQuery promise that will be resolved when * file writing completes, or rejected with a FileError. */ function writeText(fileEntry, text) { var result = new $.Deferred(); fileEntry.createWriter(function (fileWriter) { fileWriter.onwriteend = function (e) { result.resolve(); }; fileWriter.onerror = function (err) { result.reject(err); }; // TODO (issue #241): NativeFileSystem.BlobBulder fileWriter.write(text); }); return result.promise(); } /** @const */ var LINE_ENDINGS_CRLF = "CRLF"; /** @const */ var LINE_ENDINGS_LF = "LF"; /** * Returns the standard line endings for the current platform * @return {LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} */ function getPlatformLineEndings() { return brackets.platform === "win" ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF; } /** * Scans the first 1000 chars of the text to determine how it encodes line endings. Returns * null if usage is mixed or if no line endings found. * @param {!string} text * @return {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} */ function sniffLineEndings(text) { var subset = text.substr(0, 1000); // (length is clipped to text.length) var hasCRLF = /\r\n/.test(subset); var hasLF = /[^\r]\n/.test(subset); if ((hasCRLF && hasLF) || (!hasCRLF && !hasLF)) { return null; } else { return hasCRLF ? LINE_ENDINGS_CRLF : LINE_ENDINGS_LF; } } /** * Translates any line ending types in the given text to the be the single form specified * @param {!string} text * @param {null|LINE_ENDINGS_CRLF|LINE_ENDINGS_LF} lineEndings * @return {string} */ function translateLineEndings(text, lineEndings) { if (lineEndings !== LINE_ENDINGS_CRLF && lineEndings !== LINE_ENDINGS_LF) { lineEndings = getPlatformLineEndings(); } var eolStr = (lineEndings === LINE_ENDINGS_CRLF ? "\r\n" : "\n"); var findAnyEol = /\r\n|\r|\n/g; return text.replace(findAnyEol, eolStr); } function getFileErrorString(code) { // There are a few error codes that we have specific error messages for. The rest are // displayed with a generic "(error N)" message. var result; if (code === FileError.NOT_FOUND_ERR) { result = Strings.NOT_FOUND_ERR; } else if (code === FileError.NOT_READABLE_ERR) { result = Strings.NOT_READABLE_ERR; } else if (code === FileError.NO_MODIFICATION_ALLOWED_ERR) { result = Strings.NO_MODIFICATION_ALLOWED_ERR_FILE; } else { result = StringUtils.format(Strings.GENERIC_ERROR, code); } return result; } function showFileOpenError(code, path) { return Dialogs.showModalDialog( Dialogs.DIALOG_ID_ERROR, Strings.ERROR_OPENING_FILE_TITLE, StringUtils.format( Strings.ERROR_OPENING_FILE, StringUtils.htmlEscape(path), getFileErrorString(code) ) ); } /** * Convert a URI path to a native path. * On both platforms, this unescapes the URI * On windows, URI paths start with a "/", but have a drive letter ("C:"). In this * case, remove the initial "/". * @param {!string} path * @return {string} */ function convertToNativePath(path) { path = unescape(path); if (path.indexOf(":") !== -1 && path[0] === "/") { return path.substr(1); } return path; } /** * Returns a native absolute path to the 'brackets' source directory. * Note that this only works when run in brackets/src/index.html, so it does * not work for unit tests (which is run from brackets/test/SpecRunner.html) * @return {string} */ function getNativeBracketsDirectoryPath() { var pathname = decodeURI(window.location.pathname); var directory = pathname.substr(0, pathname.lastIndexOf("/")); return convertToNativePath(directory); } /** * Given the module object passed to JS module define function, * convert the path (which is relative to the current window) * to a native absolute path. * Returns a native absolute path to the module folder. * @return {string} */ function getNativeModuleDirectoryPath(module) { var path, relPath, index, pathname; if (module && module.uri) { // Remove window name from base path. Maintain trailing slash. pathname = decodeURI(window.location.pathname); path = convertToNativePath(pathname.substr(0, pathname.lastIndexOf("/") + 1)); // Remove module name from relative path. Remove trailing slash. pathname = decodeURI(module.uri); relPath = pathname.substr(0, pathname.lastIndexOf("/")); // handle leading "../" in relative directory while (relPath.substr(0, 3) === "../") { path = path.substr(0, path.length - 1); // strip trailing slash from base path index = path.lastIndexOf("/"); // find next slash from end if (index !== -1) { path = path.substr(0, index + 1); // remove last dir while maintaining slash } relPath = relPath.substr(3); // remove leading "../" from relative path } path += relPath; } return path; } // Define public API exports.LINE_ENDINGS_CRLF = LINE_ENDINGS_CRLF; exports.LINE_ENDINGS_LF = LINE_ENDINGS_LF; exports.getPlatformLineEndings = getPlatformLineEndings; exports.sniffLineEndings = sniffLineEndings; exports.translateLineEndings = translateLineEndings; exports.showFileOpenError = showFileOpenError; exports.getFileErrorString = getFileErrorString; exports.readAsText = readAsText; exports.writeText = writeText; exports.convertToNativePath = convertToNativePath; exports.getNativeBracketsDirectoryPath = getNativeBracketsDirectoryPath; exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $ */ define('language/HTMLUtils',['require','exports','module'],function (require, exports, module) { //constants var TAG_NAME = "tagName", ATTR_NAME = "attr.name", ATTR_VALUE = "attr.value"; /** * @private * moves the current context backwards by one token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _movePrevToken(ctx) { if (ctx.pos.ch <= 0 || ctx.token.start <= 0) { //move up a line if (ctx.pos.line <= 0) { return false; //at the top already } ctx.pos.line--; ctx.pos.ch = ctx.editor.getLine(ctx.pos.line).length; } else { ctx.pos.ch = ctx.token.start; } ctx.token = ctx.editor.getTokenAt(ctx.pos); return true; } /** * @private * moves the current context forward by one token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _moveNextToken(ctx) { var eol = ctx.editor.getLine(ctx.pos.line).length; if (ctx.pos.ch >= eol || ctx.token.end >= eol) { //move down a line if (ctx.pos.line >= ctx.editor.lineCount() - 1) { return false; //at the bottom } ctx.pos.line++; ctx.pos.ch = 0; } else { ctx.pos.ch = ctx.token.end + 1; } ctx.token = ctx.editor.getTokenAt(ctx.pos); return true; } /** * @private * moves the current context in the given direction, skipping any whitespace it hits * @param {function} moveFxn the funciton to move the context * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} ctx * @return {boolean} whether the context changed */ function _moveSkippingWhitespace(moveFxn, ctx) { if (!moveFxn(ctx)) { return false; } while (!ctx.token.className && ctx.token.string.trim().length === 0) { if (!moveFxn(ctx)) { return false; } } return true; } /** * @private * creates a context object * @param {CodeMirror} editor * @param {{ch:{string}, line:{number}} pos * @return {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} */ function _getInitialContext(editor, pos) { return { "editor": editor, "pos": pos, "token": editor.getTokenAt(pos) }; } /** * @private * in the given context, get the character offset of pos from the start of the token * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {number} */ function _offsetInToken(ctx) { var offset = ctx.pos.ch - ctx.token.start; if (offset < 0) { console.log("CodeHintUtils: _offsetInToken - Invalid context: the pos what not in the current token!"); } return offset; } /** * @private * Sometimes as attr values are getting typed, if the quotes aren't balanced yet * some extra 'non attribute value' text gets included in the token. This attempts * to assure the attribute value we grab is always good * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return { val:{string}, offset:{number}} */ function _extractAttrVal(ctx) { var attrValue = ctx.token.string, startChar = attrValue.charAt(0), endChar = attrValue.charAt(attrValue.length - 1), offset = _offsetInToken(ctx), foundEqualSign = false; //If this is a fully quoted value, return the whole //thing regardless of position if (attrValue.length > 1 && (startChar === "'" || startChar === '"') && endChar === startChar) { // Find an equal sign before the end quote. If found, // then the user may be entering an attribute value right before // another attribute and we're getting a false balanced string. // An example of this case is <link rel" href="foo"> where the // cursor is right after the first double quote. foundEqualSign = (attrValue.match(/\=\s*['"]$/) !== null); if (!foundEqualSign) { //strip the quotes and return; attrValue = attrValue.substring(1, attrValue.length - 1); offset = offset - 1 > attrValue.length ? attrValue.length : offset - 1; return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: true}; } } if (foundEqualSign) { var spaceIndex = attrValue.indexOf(" "); attrValue = attrValue.substring(0, (spaceIndex > offset) ? spaceIndex : offset); } else if (offset > 0) { //The att value it getting edit in progress. There is possible extra //stuff in this token state since the quote isn't closed, so we assume //the stuff from the quote to the current pos is definitely in the attribute //value. attrValue = attrValue.substring(0, offset); } //If the attrValue start with a quote, trim that now startChar = attrValue.charAt(0); if (startChar === "'" || startChar === '"') { attrValue = attrValue.substring(1); offset--; } else { startChar = ""; } return {val: attrValue, offset: offset, quoteChar: startChar, hasEndQuote: false}; } /** * @private * Gets the tagname from where ever you are in the currect state * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _extractTagName(ctx) { if (ctx.token.state.tagName) { return ctx.token.state.tagName; //XML mode } else if (ctx.token.state.htmlState) { return ctx.token.state.htmlState.tagName; //HTML mode } // Some mixed modes that offer HTML as a nested mode don't actually expose the HTML state return null; } /** * Creates a tagInfo object and assures all the values are entered or are empty strings * @param {string} tokenType what is getting edited and should be hinted * @param {number} offset where the cursor is for the part getting hinted * @param {string} tagName The name of the tag * @param {string} attrName The name of the attribute * @param {string} attrValue The value of the attribute * @return {{tagName:string, attr{name:string, value:string}, hint:{type:{string}, offset{number}}}} * A tagInfo object with some context about the current tag hint. */ function createTagInfo(tokenType, offset, tagName, attrName, attrValue, valueAssigned, quoteChar, hasEndQuote) { return { tagName: tagName || "", attr: { name: attrName || "", value: attrValue || "", valueAssigned: valueAssigned || false, quoteChar: quoteChar || "", hasEndQuote: hasEndQuote || false }, position: { tokenType: tokenType || "", offset: offset || 0 } }; } /** * @private * Gets the taginfo starting from the attribute value and moving backwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @return {string} */ function _getTagInfoStartingFromAttrValue(ctx) { // Assume we in the attr value // and validate that by going backwards var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, offset = attrInfo.offset, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote, strLength = ctx.token.string.length; if (ctx.token.className === "string" && ctx.pos.ch === ctx.token.end && strLength > 1) { var firstChar = ctx.token.string[0], lastChar = ctx.token.string[strLength - 1]; // We get here only when the cursor is immediately on the right of the end quote // of an attribute value. So we want to return an empty tag info so that the caller // can dismiss the code hint popup if it is still open. if (firstChar === lastChar && (firstChar === "'" || firstChar === "\"")) { return createTagInfo(); } } //Move to the prev token, and check if it's "=" if (!_moveSkippingWhitespace(_movePrevToken, ctx) || ctx.token.string !== "=") { return createTagInfo(); } //Move to the prev token, and check if it's an attribute if (!_moveSkippingWhitespace(_movePrevToken, ctx) || ctx.token.className !== "attribute") { return createTagInfo(); } var attrName = ctx.token.string; var tagName = _extractTagName(ctx); //We're good. return createTagInfo(ATTR_VALUE, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * @private * Gets the taginfo starting from the attribute name and moving forwards * @param {editor:{CodeMirror}, pos:{ch:{string}, line:{number}}, token:{object}} context * @param {boolean} isPriorAttr indicates whether we're getting info for a prior attribute * @return {string} */ function _getTagInfoStartingFromAttrName(ctx, isPriorAttr) { //Verify We're in the attribute name, move forward and try to extract the rest of //the info. If the user it typing the attr the rest might not be here if (isPriorAttr === false && ctx.token.className !== "attribute") { return createTagInfo(); } var tagName = _extractTagName(ctx); var attrName = ctx.token.string; var offset = _offsetInToken(ctx); if (!_moveSkippingWhitespace(_moveNextToken, ctx) || ctx.token.string !== "=") { return createTagInfo(ATTR_NAME, offset, tagName, attrName); } if (!_moveSkippingWhitespace(_moveNextToken, ctx)) { return createTagInfo(ATTR_NAME, offset, tagName, attrName); } //this should be the attrvalue var attrInfo = _extractAttrVal(ctx), attrVal = attrInfo.val, quoteChar = attrInfo.quoteChar, hasEndQuote = attrInfo.hasEndQuote; return createTagInfo(ATTR_NAME, offset, tagName, attrName, attrVal, true, quoteChar, hasEndQuote); } /** * Figure out if we're in a tag, and if we are return info about what to hint about it * An example token stream for this tag is <span id="open-files-disclosure-arrow"></span> : * className:tag string:"<span" * className: string:" " * className:attribute string:"id" * className: string:"=" * className:string string:""open-files-disclosure-arrow"" * className:tag string:"></span>" * @param {Editor} editor An instance of a Brackets editor * @param {{ch: number, line: number}} constPos A CM pos (likely from editor.getCursor()) * @return {{tagName:string, attr{name:string, value:string}, hint:{type:{string}, offset{number}}}} * A tagInfo object with some context about the current tag hint. */ function getTagInfo(editor, constPos) { //we're going to changing pos a lot, but we don't want to mess up //the pos the caller passed in so we use extend to make a safe copy of it. //This is what pass by value in c++ would do. var pos = $.extend({}, constPos), ctx = _getInitialContext(editor._codeMirror, pos), offset = _offsetInToken(ctx), tagInfo, tokenType; // check if this is inside a style block. if (editor.getModeForSelection() !== "html") { return createTagInfo(); } //check and see where we are in the tag if (ctx.token.string.length > 0 && ctx.token.string.trim().length === 0) { // token at (i.e. before) pos is whitespace, so test token at next pos // // note: getTokenAt() does range checking for ch. If it detects that ch is past // EOL, it uses EOL, same token is returned, and the following condition fails, // so we don't need to worry about testPos being valid. var testPos = {ch: ctx.pos.ch + 1, line: ctx.pos.line}, testToken = editor._codeMirror.getTokenAt(testPos); if (testToken.string.length > 0 && testToken.string.trim().length > 0 && testToken.string.charAt(0) !== ">") { // pos has whitespace before it and non-whitespace after it, so use token after ctx.token = testToken; if (ctx.token.className === "tag") { // check to see if the cursor is just before a "<" but not in any tag. if (ctx.token.string.charAt(0) === "<") { return createTagInfo(); } } else if (ctx.token.className === "attribute") { // check to see if the user is going to add a new attr before an existing one return _getTagInfoStartingFromAttrName(ctx, false); } else if (ctx.token.className === "string") { // we're either before a "=" or an attribute value. return createTagInfo(); } ctx.pos = testPos; // Get the new offset from test token and subtract one for testPos adjustment offset = _offsetInToken(ctx) - 1; } else { // We get here if ">" or white spaces after testPos. // next, see what's before pos if (!_movePrevToken(ctx)) { return createTagInfo(); } if (ctx.token.className !== "tag") { //if wasn't the tag name, assume it was an attr value tagInfo = _getTagInfoStartingFromAttrValue(ctx); //if it wasn't an attr value, assume it was an empty attr (ie. attr with no value) if (!tagInfo.tagName) { tagInfo = _getTagInfoStartingFromAttrName(ctx, true); } //We don't want to give context for the previous attr //and we want it to look like the user is going to add a new attr if (tagInfo.tagName) { return createTagInfo(ATTR_NAME, 0, tagInfo.tagName); } return createTagInfo(); } //we know the tag was here, so they user is adding an attr name tokenType = ATTR_NAME; offset = 0; } } if (ctx.token.className === "tag") { // Check if the user just typed a white space after "<" that made an existing tag invalid. if (ctx.token.string.indexOf("< ") === 0) { return createTagInfo(); } //check to see if this is the closing of a tag (either the start or end) if (ctx.token.string === ">" || (ctx.token.string.charAt(0) === "<" && ctx.token.string.charAt(1) === "/")) { return createTagInfo(); } if (!tokenType) { tokenType = TAG_NAME; offset--; //need to take off 1 for the leading "<" } //we're actually in the tag, just return that as we have no relevant //info about what attr is selected return createTagInfo(tokenType, offset, _extractTagName(ctx)); } if (ctx.token.string === "=") { // To discourage unquoted attribute value usage we intentionally return an invalid tag info here. // This will also spare us from handling the conversion between quoted and unquoted attribute values. return createTagInfo(); } if (ctx.token.className === "attribute") { tagInfo = _getTagInfoStartingFromAttrName(ctx, false); } else { // if we're not at a tag, "=", or attribute name, assume we're in the value tagInfo = _getTagInfoStartingFromAttrValue(ctx); } if (tokenType && tagInfo.tagName) { tagInfo.position.tokenType = tokenType; tagInfo.position.offset = offset; } return tagInfo; } /** * Returns an Array of info about all <style> blocks in the given Editor's HTML document (assumes * the Editor contains HTML text). * @param {!Editor} editor */ function findStyleBlocks(editor) { // Start scanning from beginning of file var ctx = _getInitialContext(editor._codeMirror, {line: 0, ch: 0}); var styleBlocks = []; var currentStyleBlock = null; var inStyleBlock = false; while (_moveNextToken(ctx)) { if (inStyleBlock) { // Check for end of this <style> block if (ctx.token.state.mode !== "css") { currentStyleBlock.text = editor.document.getRange(currentStyleBlock.start, currentStyleBlock.end); inStyleBlock = false; } else { currentStyleBlock.end = { line: ctx.pos.line, ch: ctx.pos.ch }; } } else { // Check for start of a <style> block if (ctx.token.state.mode === "css") { currentStyleBlock = { start: { line: ctx.pos.line, ch: ctx.pos.ch } }; styleBlocks.push(currentStyleBlock); inStyleBlock = true; } // else, random token in non-CSS content: ignore } } return styleBlocks; } // Define public API exports.TAG_NAME = TAG_NAME; exports.ATTR_NAME = ATTR_NAME; exports.ATTR_VALUE = ATTR_VALUE; exports.getTagInfo = getTagInfo; //The createTagInfo is really only for the unit tests so they can make the same structure to //compare results with exports.createTagInfo = createTagInfo; exports.findStyleBlocks = findStyleBlocks; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window */ /** * Utilities for managing pop-ups. */ define('widgets/PopUpManager',['require','exports','module','editor/EditorManager'],function (require, exports, module) { var EditorManager = require("editor/EditorManager"); var _popUps = []; /** * Add Esc key handling for a popup DOM element. * * @param {!jQuery} $popUp jQuery object for the DOM element pop-up * @param {function} removeHandler Pop-up specific remove (e.g. display:none or DOM removal) * @param {?Boolean} autoAddRemove - Specify true to indicate the PopUpManager should * add/remove the popup from the DOM when the popup is open/closed. Specify false * when the popup is either always persistant in the DOM or the add/remove is handled * external to the PopupManager * */ function addPopUp($popUp, removeHandler, autoAddRemove) { autoAddRemove = autoAddRemove || false; _popUps.push($popUp[0]); $popUp.data("PopUpManager-autoAddRemove", autoAddRemove); $popUp.data("PopUpManager-removeHandler", removeHandler); if (autoAddRemove) { $(window.document.body).append($popUp); } } /** * Remove Esc key handling for a pop-up. Removes the pop-up from the DOM * if the pop-up is currently visible and was not originally attached. * * @param {!jQuery} $popUp */ function removePopUp($popUp) { var index = _popUps.indexOf($popUp[0]); if (index >= 0) { var autoAddRemove = $popUp.data("PopUpManager-autoAddRemove"), removeHandler = $popUp.data("PopUpManager-removeHandler"); if (removeHandler && $popUp.find(":visible").length > 0) { removeHandler(); } if (autoAddRemove) { $popUp.remove(); _popUps = _popUps.slice(index); } } } function _keydownCaptureListener(keyEvent) { if (keyEvent.keyCode !== 27) { // escape key return; } // allow the popUp to prevent closing var $popUp, i, event = new $.Event("popUpClose"); for (i = _popUps.length - 1; i >= 0; i--) { $popUp = $(_popUps[i]); if ($popUp.find(":visible").length > 0) { $popUp.trigger(event); if (!event.isDefaultPrevented()) { // Stop the DOM event from propagating keyEvent.stopImmediatePropagation(); removePopUp($popUp); // TODO: right now Menus and Context Menus do not take focus away from // the editor. We need to have a focus manager to correctly manage focus // between editors and other UI elements. // For now we don't set focus here and assume individual popups // adjust focus if necessary // See story in Trello card #404 //EditorManager.focusEditor(); } break; } } } window.document.body.addEventListener("keydown", _keydownCaptureListener, true); exports.addPopUp = addPopUp; exports.removePopUp = removePopUp; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window */ define('utils/ViewUtils',['require','exports','module'],function (require, exports, module) { var SCROLL_SHADOW_HEIGHT = 5; /** * @private */ var _resizeHandlers = []; /** * Positions shadow background elements to indicate vertical scrolling. * @param {!DOMElement} $displayElement the DOMElement that displays the shadow * @param {!Object} $scrollElement the object that is scrolled * @param {!DOMElement} $shadowTop div .scroller-shadow.top * @param {!DOMElement} $shadowBottom div .scroller-shadow.bottom * @param {boolean} isPositionFixed When using absolute position, top remains at 0. */ function _updateScrollerShadow($displayElement, $scrollElement, $shadowTop, $shadowBottom, isPositionFixed) { var offsetTop = 0, scrollElement = $scrollElement.get(0), scrollTop = scrollElement.scrollTop, topShadowOffset = Math.min(scrollTop - SCROLL_SHADOW_HEIGHT, 0), displayElementWidth = $displayElement.width(); if ($shadowTop) { $shadowTop.css("background-position", "0px " + topShadowOffset + "px"); if (isPositionFixed) { offsetTop = $displayElement.offset().top; $shadowTop.css("top", offsetTop); } if (isPositionFixed) { $shadowTop.css("width", displayElementWidth); } } if ($shadowBottom) { var clientHeight = scrollElement.clientHeight, outerHeight = $displayElement.outerHeight(), scrollHeight = scrollElement.scrollHeight, bottomOffset = outerHeight - clientHeight, bottomShadowOffset = SCROLL_SHADOW_HEIGHT; // outside of shadow div viewport if (scrollHeight > clientHeight) { bottomShadowOffset -= Math.min(SCROLL_SHADOW_HEIGHT, (scrollHeight - (scrollTop + clientHeight))); } $shadowBottom.css("background-position", "0px " + bottomShadowOffset + "px"); $shadowBottom.css("top", offsetTop + outerHeight - SCROLL_SHADOW_HEIGHT); $shadowBottom.css("width", displayElementWidth); } } function getOrCreateShadow($displayElement, position, isPositionFixed) { var $findShadow = $displayElement.find(".scroller-shadow." + position); if ($findShadow.length === 0) { $findShadow = $(window.document.createElement("div")).addClass("scroller-shadow " + position); $displayElement.append($findShadow); } if (!isPositionFixed) { // position is fixed by default $findShadow.css("position", "absolute"); $findShadow.css(position, "0"); } return $findShadow; } /** * Installs event handlers for updatng shadow background elements to indicate vertical scrolling. * @param {!DOMElement} displayElement the DOMElement that displays the shadow. Must fire * "contentChanged" events when the element is resized or repositioned. * @param {?Object} scrollElement the object that is scrolled. Must fire "scroll" events * when the element is scrolled. If null, the displayElement is used. * @param {?boolean} showBottom optionally show the bottom shadow */ function addScrollerShadow(displayElement, scrollElement, showBottom) { // use fixed positioning when the display and scroll elements are the same var isPositionFixed = false; if (!scrollElement) { scrollElement = displayElement; isPositionFixed = true; } // update shadows when the scrolling element is scrolled var $displayElement = $(displayElement), $scrollElement = $(scrollElement); var $shadowTop = getOrCreateShadow($displayElement, "top", isPositionFixed); var $shadowBottom = (showBottom) ? getOrCreateShadow($displayElement, "bottom", isPositionFixed) : null; var doUpdate = function () { _updateScrollerShadow($displayElement, $scrollElement, $shadowTop, $shadowBottom, isPositionFixed); }; $scrollElement.on("scroll.scroller-shadow", doUpdate); $displayElement.on("contentChanged.scroller-shadow", doUpdate); // update immediately doUpdate(); } /** * Remove scroller-shadow effect. * @param {!DOMElement} displayElement the DOMElement that displays the shadow * @param {?Object} scrollElement the object that is scrolled */ function removeScrollerShadow(displayElement, scrollElement) { if (!scrollElement) { scrollElement = displayElement; } var $displayElement = $(displayElement), $scrollElement = $(scrollElement); // remove scrollerShadow elements from DOM $displayElement.find(".scroller-shadow.top").remove(); $displayElement.find(".scroller-shadow.bottom").remove(); // remove event handlers $scrollElement.off("scroll.scroller-shadow"); $displayElement.off("contentChanged.scroller-shadow"); } /** * Within a scrolling DOMElement, creates and positions a styled selection * div to align a single selected list item from a ul list element. * * Assumptions: * - scrollElement is a child of the #file-section div * - ul list element fires a "selectionChanged" event after the * selectedClassName is assigned to a new list item * * @param {!DOMElement} scrollElement A DOMElement containing a ul list element * @param {!string} selectedClassName A CSS class name on at most one list item in the contained list */ function sidebarList($scrollerElement, selectedClassName, leafClassName) { var $listElement = $scrollerElement.find("ul"), $selectionMarker, $selectionTriangle, $sidebar = $("#sidebar"), showTriangle = true; // build selectionMarker and position absolute within the scroller $selectionMarker = $(window.document.createElement("div")).addClass("sidebar-selection"); $scrollerElement.prepend($selectionMarker); // enable scrolling $scrollerElement.css("overflow", "auto"); // use relative postioning for clipping the selectionMarker within the scrollElement $scrollerElement.css("position", "relative"); // build selectionTriangle and position fixed to the window $selectionTriangle = $(window.document.createElement("div")).addClass("sidebar-selection-triangle"); $scrollerElement.append($selectionTriangle); selectedClassName = "." + (selectedClassName || "selected"); var updateSelectionTriangle = function () { var selectionMarkerHeight = $selectionMarker.height(), selectionMarkerOffset = $selectionMarker.offset(), scrollerOffset = $scrollerElement.offset(), triangleHeight = $selectionTriangle.outerHeight(), scrollerTop = scrollerOffset.top, scrollerBottom = scrollerTop + $scrollerElement.outerHeight(), scrollerLeft = scrollerOffset.left, triangleTop = selectionMarkerOffset.top; $selectionTriangle.css("top", triangleTop); $selectionTriangle.css("left", $sidebar.width() - $selectionTriangle.outerWidth()); $selectionTriangle.toggleClass("triangle-visible", showTriangle); var triangleClipOffsetYBy = Math.floor((selectionMarkerHeight - triangleHeight) / 2), triangleBottom = triangleTop + triangleHeight + triangleClipOffsetYBy; if (triangleTop < scrollerTop || triangleBottom > scrollerBottom) { $selectionTriangle.css("clip", "rect(" + Math.max(scrollerTop - triangleTop - triangleClipOffsetYBy, 0) + "px, auto, " + (triangleHeight - Math.max(triangleBottom - scrollerBottom, 0)) + "px, auto)"); } else { $selectionTriangle.css("clip", ""); } }; var updateSelectionMarker = function (event, reveal) { // find the selected list item var $listItem = $listElement.find(selectedClassName).closest("li"); if (leafClassName) { showTriangle = $listItem.hasClass(leafClassName); } // always hide selection visuals first to force layout (issue #719) $selectionTriangle.hide(); $selectionMarker.hide(); if ($listItem.length === 1) { // list item position is relative to scroller var selectionMarkerTop = $listItem.offset().top - $scrollerElement.offset().top + $scrollerElement.get(0).scrollTop; // force selection width to match scroller $selectionMarker.width($scrollerElement.get(0).scrollWidth); // move the selectionMarker position to align with the list item $selectionMarker.css("top", selectionMarkerTop); $selectionMarker.show(); updateSelectionTriangle(); $selectionTriangle.show(); // fully scroll to the selectionMarker if it's not initially in the viewport var scrollerElement = $scrollerElement.get(0), scrollerHeight = scrollerElement.clientHeight, selectionMarkerHeight = $selectionMarker.height(), selectionMarkerBottom = selectionMarkerTop + selectionMarkerHeight, currentScrollBottom = scrollerElement.scrollTop + scrollerHeight; // update scrollTop to reveal the selected list item if (reveal) { if (selectionMarkerTop >= currentScrollBottom) { $listItem.get(0).scrollIntoView(false); } else if (selectionMarkerBottom <= scrollerElement.scrollTop) { $listItem.get(0).scrollIntoView(true); } } } }; $listElement.on("selectionChanged", updateSelectionMarker); $scrollerElement.on("scroll", updateSelectionTriangle); // update immediately updateSelectionMarker(); // update clipping when the window resizes _resizeHandlers.push(updateSelectionTriangle); } /** * @private */ function _handleResize() { _resizeHandlers.forEach(function (f) { f.apply(); }); } /** * Within a scrolling DOMElement, if necessary, scroll element into viewport. * * To Perform the minimum amount of scrolling necessary, cases should be handled as follows: * - element already completely in view : no scrolling * - element above viewport : scroll view so element is at top * - element left of viewport : scroll view so element is at left * - element below viewport : scroll view so element is at bottom * - element right of viewport : scroll view so element is at right * * Assumptions: * - $view is a scrolling container * * @param {!DOMElement} $view - A jQuery scrolling container * @param {!DOMElement} $element - A jQuery element * @param {?boolean} scrollHorizontal - whether to also scroll horizonally */ function scrollElementIntoView($view, $element, scrollHorizontal) { var viewOffset = $view.offset(), viewScroller = $view.get(0), element = $element.get(0), elementOffset = $element.offset(); // scroll minimum amount if (elementOffset.top + $element.height() >= (viewOffset.top + $view.height())) { // below viewport element.scrollIntoView(false); } else if (elementOffset.top <= viewOffset.top) { // above viewport element.scrollIntoView(true); } if (scrollHorizontal) { if (elementOffset.left < 0) { $view.scrollLeft($view.scrollLeft() + elementOffset.left); } else if (elementOffset.left + $element.width() >= viewOffset.left + $view.width()) { $view.scrollLeft(elementOffset.left - viewOffset.left); } } } // handle all resize handlers in a single listener $(window).resize(_handleResize); // Define public API exports.SCROLL_SHADOW_HEIGHT = SCROLL_SHADOW_HEIGHT; exports.addScrollerShadow = addScrollerShadow; exports.removeScrollerShadow = removeScrollerShadow; exports.sidebarList = sidebarList; exports.scrollElementIntoView = scrollElementIntoView; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, window, brackets */ define('editor/CodeHintManager',['require','exports','module','language/HTMLUtils','command/Menus','utils/StringUtils','editor/EditorManager','widgets/PopUpManager','utils/ViewUtils'],function (require, exports, module) { // Load dependent modules var HTMLUtils = require("language/HTMLUtils"), Menus = require("command/Menus"), StringUtils = require("utils/StringUtils"), EditorManager = require("editor/EditorManager"), PopUpManager = require("widgets/PopUpManager"), ViewUtils = require("utils/ViewUtils"); var hintProviders = [], hintList, shouldShowHintsOnKeyUp = false; /** * @constructor * * Displays a popup list of code completions. * Currently only HTML tags are supported, but this will greatly be extended in coming sprint * to include: extensibility API, HTML attributes hints, JavaScript hints, CSS hints */ function CodeHintList() { this.currentProvider = null; this.query = {queryStr: null}; this.displayList = []; this.options = { maxResults: 999 }; this.opened = false; this.selectedIndex = -1; this.editor = null; this.$hintMenu = $("<li class='dropdown codehint-menu'></li>"); var $toggle = $("<a href='#' class='dropdown-toggle'></a>") .hide(); this.$hintMenu.append($toggle) .append("<ul class='dropdown-menu'></ul>"); } /** * @private * Enters the code completion text into the editor * @string {string} completion - text to insert into current code editor */ CodeHintList.prototype._handleItemClick = function (completion) { this.currentProvider.handleSelect(completion, this.editor, this.editor.getCursorPos()); this.close(); }; /** * Adds a single item to the hint list * @param {string} name */ CodeHintList.prototype.addItem = function (name) { var self = this; var displayName = name.replace( new RegExp(StringUtils.regexEscape(this.query.queryStr), "i"), "<strong>$&</strong>" ); var $item = $("<li><a href='#'><span class='codehint-item'>" + displayName + "</span></a></li>") .on("click", function (e) { // Don't let the click propagate upward (otherwise it will hit the close handler in // bootstrap-dropdown). e.stopPropagation(); self._handleItemClick(name); }); this.$hintMenu.find("ul.dropdown-menu") .append($item); }; /** * Rebuilds the hint list based on this.query */ CodeHintList.prototype.updateList = function () { this.displayList = this.currentProvider.search(this.query); this.buildListView(); }; /** * Removes all list items from hint list */ CodeHintList.prototype.clearList = function () { this.$hintMenu.find("li").remove(); }; /** * Rebuilds the list items for the hint list based on this.displayList */ CodeHintList.prototype.buildListView = function () { this.clearList(); var self = this; var count = 0; $.each(this.displayList, function (index, item) { if (count > self.options.maxResults) { return false; } self.addItem(item); count++; }); if (count === 0) { this.close(); } else { // Select the first item in the list this.setSelectedIndex(0); } }; /** * Selects the item in the hint list specified by index * @param {Number} index */ CodeHintList.prototype.setSelectedIndex = function (index) { var items = this.$hintMenu.find("li"); // Range check index = Math.max(0, Math.min(index, items.length - 1)); // Clear old highlight if (this.selectedIndex !== -1) { $(items[this.selectedIndex]).find("a").removeClass("highlight"); } // Highlight the new selected item this.selectedIndex = index; if (this.selectedIndex !== -1) { var $item = $(items[this.selectedIndex]); var $view = this.$hintMenu.find("ul.dropdown-menu"); ViewUtils.scrollElementIntoView($view, $item, false); $item.find("a").addClass("highlight"); } }; /** * Handles key presses when the hint list is being displayed * @param {Editor} editor * @param {KeyBoardEvent} keyEvent */ CodeHintList.prototype.handleKeyEvent = function (editor, event) { var keyCode = event.keyCode; // Up arrow, down arrow and enter key are always handled here if (event.type !== "keypress" && (keyCode === 38 || keyCode === 40 || keyCode === 13 || keyCode === 33 || keyCode === 34)) { if (event.type === "keydown") { if (keyCode === 38) { // Up arrow this.setSelectedIndex(this.selectedIndex - 1); } else if (keyCode === 40) { // Down arrow this.setSelectedIndex(this.selectedIndex + 1); } else if (keyCode === 33) { // Page Up this.setSelectedIndex(this.selectedIndex - this.getItemsPerPage()); } else if (keyCode === 34) { // Page Down this.setSelectedIndex(this.selectedIndex + this.getItemsPerPage()); } else { // Enter/return key // Trigger a click handler to commmit the selected item $(this.$hintMenu.find("li")[this.selectedIndex]).triggerHandler("click"); } } event.preventDefault(); return; } // All other key events trigger a rebuild of the list, but only // on keyup events if (event.type !== "keyup") { return; } this.query = this.currentProvider.getQueryInfo(this.editor, this.editor.getCursorPos()); this.updateList(); // Update the CodeHintList location if (this.displayList.length) { var hintPos = this.calcHintListLocation(); this.$hintMenu.css({"left": hintPos.left, "top": hintPos.top}); } }; /** * Return true if the CodeHintList is open. * @return {Boolean} */ CodeHintList.prototype.isOpen = function () { // We don't get a notification when the dropdown closes. The best // we can do is keep an "opened" flag and check to see if we // still have the "open" class applied. if (this.opened && !this.$hintMenu.hasClass("open")) { this.opened = false; } return this.opened; }; /** * Displays the hint list at the current cursor position * @param {Editor} editor */ CodeHintList.prototype.open = function (editor) { var self = this; this.editor = editor; Menus.closeAll(); this.currentProvider = null; $.each(hintProviders, function (index, item) { var query = item.getQueryInfo(self.editor, self.editor.getCursorPos()); if (query.queryStr !== null) { self.query = query; self.currentProvider = item; return false; } }); if (!this.currentProvider) { return; } this.updateList(); if (this.displayList.length) { // Need to add the menu to the DOM before trying to calculate its ideal location. $("#codehint-menu-bar > ul").append(this.$hintMenu); var hintPos = this.calcHintListLocation(); this.$hintMenu.addClass("open") .css({"left": hintPos.left, "top": hintPos.top}); this.opened = true; PopUpManager.addPopUp(this.$hintMenu, function () { self.close(); }); } }; /** * Closes the hint list */ CodeHintList.prototype.close = function () { // TODO: Due to #1381, this won't get called if the user clicks out of the code hint menu. // That's (sort of) okay right now since it doesn't really matter if a single old invisible // code hint list is lying around (it'll get closed the next time the user pops up a code // hint). Once #1381 is fixed this issue should go away. this.$hintMenu.removeClass("open"); this.opened = false; this.currentProvider = null; PopUpManager.removePopUp(this.$hintMenu); this.$hintMenu.remove(); if (hintList === this) { hintList = null; } }; /** * Computes top left location for hint list so that the list is not clipped by the window * @return {Object.<left: Number, top: Number> } */ CodeHintList.prototype.calcHintListLocation = function () { var cursor = this.editor._codeMirror.cursorCoords(), posTop = cursor.y, posLeft = cursor.x, $window = $(window), $menuWindow = this.$hintMenu.children("ul"); // TODO Ty: factor out menu repositioning logic so code hints and Context menus share code // adjust positioning so menu is not clipped off bottom or right var bottomOverhang = posTop + 25 + $menuWindow.height() - $window.height(); if (bottomOverhang > 0) { posTop -= (27 + $menuWindow.height()); } // todo: should be shifted by line height posTop -= 15; // shift top for hidden parent element //posLeft += 5; var rightOverhang = posLeft + $menuWindow.width() - $window.width(); if (rightOverhang > 0) { posLeft = Math.max(0, posLeft - rightOverhang); } return {left: posLeft, top: posTop}; }; /** * @private * Calculate the number of items per scroll page. Used for PageUp and PageDown. * @return {number} */ CodeHintList.prototype.getItemsPerPage = function () { var itemsPerPage = 1, $items = this.$hintMenu.find("li"), $view = this.$hintMenu.find("ul.dropdown-menu"), itemHeight; if ($items.length !== 0) { itemHeight = $($items[0]).height(); if (itemHeight) { // round down to integer value itemsPerPage = Math.floor($view.height() / itemHeight); itemsPerPage = Math.max(1, Math.min(itemsPerPage, $items.length)); } } return itemsPerPage; }; /** * Show the code hint list near the current cursor position for the specified editor * @param {Editor} */ function showHint(editor) { if (hintList) { hintList.close(); } hintList = new CodeHintList(); hintList.open(editor); } /** * Handles keys related to displaying, searching, and navigating the hint list * @param {Editor} editor * @param {KeyboardEvent} event */ function handleKeyEvent(editor, event) { var provider = null; // Check for Control+Space if (event.type === "keydown" && event.keyCode === 32 && event.ctrlKey) { showHint(editor); event.preventDefault(); } else if (event.type === "keypress") { // Check if any provider wants to show hints on this key. $.each(hintProviders, function (index, item) { if (item.shouldShowHintsOnKey(String.fromCharCode(event.charCode))) { provider = item; return false; } }); shouldShowHintsOnKeyUp = !!provider; } else if (event.type === "keyup") { if (shouldShowHintsOnKeyUp) { shouldShowHintsOnKeyUp = false; showHint(editor); } } // Pass to the hint list, if it's open if (hintList && hintList.isOpen()) { shouldShowHintsOnKeyUp = false; hintList.handleKeyEvent(editor, event); } } /** * Registers an object that is able to provide code hints. When the user requests a code * hint getQueryInfo() will be called on every hint provider. Providers should examine * the state of the editor and return a search query object with a filter string if hints * can be provided. search() will then be called allowing the hint provider to create a * list of hints for the search query. When the user chooses a hint handleSelect() is called * so that the hint provider can insert the hint into the editor. * * @param {Object.< getQueryInfo: function(editor, cursor), * search: function(string), * handleSelect: function(string, Editor, cursor), * shouldShowHintsOnKey: function(string)>} * * Parameter Details: * - getQueryInfo - examines cursor location of editor and returns an object representing * the search query to be used for hinting. queryStr is a required property of the search object * and a client may provide other properties on the object to carry more context about the query. * - search - takes a query object and returns an array of hint strings based on the queryStr property * of the query object. * - handleSelect - takes a completion string and inserts it into the editor near the cursor * position * - shouldShowHintsOnKey - inspects the char code and returns true if it wants to show code hints on that key. */ function registerHintProvider(providerInfo) { hintProviders.push(providerInfo); } /** * Expose CodeHintList for unit testing */ function _getCodeHintList() { return hintList; } // Define public API exports.handleKeyEvent = handleKeyEvent; exports.showHint = showHint; exports._getCodeHintList = _getCodeHintList; exports.registerHintProvider = registerHintProvider; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror */ /** */ define('document/TextRange',['require','exports','module'],function (require, exports, module) { /** * @constructor * * Stores a range of lines that is automatically maintained as the Document changes. The range * MAY drop out of sync with the Document in certain edge cases; startLine & endLine will become * null when that happens. * * Important: you must dispose() a TextRange when you're done with it. Because TextRange addRef()s * the Document (in order to listen to it), you will leak Documents otherwise. * * TextRange dispatches two events: * - change -- When the range changes (due to a Document change) * - lostSync -- When the backing Document changes in such a way that the range can no longer * accurately be maintained. Generally, occurs whenever an edit spans a range boundary. * After this, startLine & endLine will be unusable (set to null). * These events only ever occur in response to Document changes, so if you are already listening * to the Document, you could ignore the TextRange events and just read its updated value in your * own Document change handler. * * @param {!Document} document * @param {number} startLine First line in range (0-based, inclusive) * @param {number} endLine Last line in range (0-based, inclusive) */ function TextRange(document, startLine, endLine) { this.startLine = startLine; this.endLine = endLine; this.document = document; document.addRef(); // store this-bound version of listener so we can remove them later this._handleDocumentChange = this._handleDocumentChange.bind(this); $(document).on("change", this._handleDocumentChange); } /** Detaches from the Document. The TextRange will no longer update or send change events */ TextRange.prototype.dispose = function (editor, change) { // Disconnect from Document this.document.releaseRef(); $(this.document).off("change", this._handleDocumentChange); }; /** @type {!Document} */ TextRange.prototype.document = null; /** @type {?number} Null after "lostSync" is dispatched */ TextRange.prototype.startLine = null; /** @type {?number} Null after "lostSync" is dispatched */ TextRange.prototype.endLine = null; /** * Applies a single Document change object (out of the linked list of multiple such objects) * to this range. Returns true if the range was changed as a result. */ TextRange.prototype._applySingleChangeToRange = function (change) { // console.log(this + " applying change to (" + // (change.from && (change.from.line+","+change.from.ch)) + " - " + // (change.to && (change.to.line+","+change.to.ch)) + ")"); // Special case: the range is no longer meaningful since the entire text was replaced if (!change.from || !change.to) { this.startLine = null; this.endLine = null; return true; // Special case: certain changes around the edges of the range are problematic, because // if they're undone, we'll be unable to determine how to fix up the range to include the // undone content. (The "undo" will just look like an insertion outside our bounds.) So // in those cases, we destroy the range instead of fixing it up incorrectly. The specific // cases are: // 1. Edit crosses the start boundary of the inline editor (defined as character 0 // of the first line). // 2. Edit crosses the end boundary of the inline editor (defined as the newline at // the end of the last line). // Note: we also used to disallow edits that start at the beginning of the range (character 0 // of the first line) if they crossed a newline. This was a vestige from before case #1 // was added; now that edits crossing the top boundary (actually, undos of such edits) are // out of the picture, edits on the first line of the range unambiguously belong inside it. } else if ((change.from.line < this.startLine && change.to.line >= this.startLine) || (change.from.line <= this.endLine && change.to.line > this.endLine)) { this.startLine = null; this.endLine = null; return true; // Normal case: update the range end points if any content was added before them. Note that // we don't rely on line handles for this since we want to gracefully handle cases where the // start or end line was deleted during a change. } else { var numAdded = change.text.length - (change.to.line - change.from.line + 1); var hasChanged = false; // This logic is so simple because we've already excluded all cases where the change // crosses the range boundaries if (change.to.line < this.startLine) { this.startLine += numAdded; hasChanged = true; } if (change.to.line <= this.endLine) { this.endLine += numAdded; hasChanged = true; } // console.log("Now " + this); return hasChanged; } }; /** * Updates the range based on the changeList from a Document "change" event. Dispatches a * "change" event if the range was adjusted at all. Dispatches a "lostSync" event instead if the * range can no longer be accurately maintained. */ TextRange.prototype._applyChangesToRange = function (changeList) { var hasChanged = false; var change; for (change = changeList; change; change = change.next) { // Apply this step of the change list var result = this._applySingleChangeToRange(change); hasChanged = hasChanged || result; // If we lost sync with the range, just bail now if (this.startLine === null || this.endLine === null) { $(this).triggerHandler("lostSync"); break; } } if (hasChanged) { $(this).triggerHandler("change"); } }; TextRange.prototype._handleDocumentChange = function (event, doc, changeList) { this._applyChangesToRange(changeList); }; /* (pretty toString(), to aid debugging) */ TextRange.prototype.toString = function () { return "[TextRange " + this.startLine + "-" + this.endLine + " in " + this.document + "]"; }; // Define public API exports.TextRange = TextRange; }); /* * Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, CodeMirror, window */ /** * Editor is a 1-to-1 wrapper for a CodeMirror editor instance. It layers on Brackets-specific * functionality and provides APIs that cleanly pass through the bits of CodeMirror that the rest * of our codebase may want to interact with. An Editor is always backed by a Document, and stays * in sync with its content; because Editor keeps the Document alive, it's important to always * destroy() an Editor that's going away so it can release its Document ref. * * For now, there's a distinction between the "master" Editor for a Document - which secretly acts * as the Document's internal model of the text state - and the multitude of "slave" secondary Editors * which, via Document, sync their changes to and from that master. * * For now, direct access to the underlying CodeMirror object is still possible via _codeMirror -- * but this is considered deprecated and may go away. * * The Editor object dispatches the following events: * - keyEvent -- When any key event happens in the editor (whether it changes the text or not). * Event handlers are passed ({Editor}, {KeyboardEvent}). The 2nd arg is the raw DOM event. * Note: most listeners will only want to respond when event.type === "keypress". * - cursorActivity -- When the user moves the cursor or changes the selection, or an edit occurs. * Note: do not listen to this in order to be generally informed of edits--listen to the * "change" event on Document instead. * - scroll -- When the editor is scrolled, either by user action or programmatically. * - lostContent -- When the backing Document changes in such a way that this Editor is no longer * able to display accurate text. This occurs if the Document's file is deleted, or in certain * Document->editor syncing edge cases that we do not yet support (the latter cause will * eventually go away). * * The Editor also dispatches "change" events internally, but you should listen for those on * Documents, not Editors. * * These are jQuery events, so to listen for them you do something like this: * $(editorInstance).on("eventname", handler); */ define('editor/Editor',['require','exports','module','editor/EditorManager','editor/CodeHintManager','command/Commands','command/CommandManager','command/Menus','utils/PerfUtils','strings','document/TextRange','utils/ViewUtils'],function (require, exports, module) { var EditorManager = require("editor/EditorManager"), CodeHintManager = require("editor/CodeHintManager"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), Menus = require("command/Menus"), PerfUtils = require("utils/PerfUtils"), Strings = require("strings"), TextRange = require("document/TextRange").TextRange, ViewUtils = require("utils/ViewUtils"); /** * @private * Handle Tab key press. * @param {!CodeMirror} instance CodeMirror instance. */ function _handleTabKey(instance) { // Tab key handling is done as follows: // 1. If the selection is before any text and the indentation is to the left of // the proper indentation then indent it to the proper place. Otherwise, // add another tab. In either case, move the insertion point to the // beginning of the text. // 2. If the selection is after the first non-space character, and is not an // insertion point, indent the entire line(s). // 3. If the selection is after the first non-space character, and is an // insertion point, insert a tab character or the appropriate number // of spaces to pad to the nearest tab boundary. var from = instance.getCursor(true), to = instance.getCursor(false), line = instance.getLine(from.line), indentAuto = false, insertTab = false; if (from.line === to.line) { if (line.search(/\S/) > to.ch || to.ch === 0) { indentAuto = true; } } if (indentAuto) { var currentLength = line.length; CodeMirror.commands.indentAuto(instance); // If the amount of whitespace didn't change, insert another tab if (instance.getLine(from.line).length === currentLength) { insertTab = true; to.ch = 0; } } else if (instance.somethingSelected()) { CodeMirror.commands.indentMore(instance); } else { insertTab = true; } if (insertTab) { if (instance.getOption("indentWithTabs")) { CodeMirror.commands.insertTab(instance); } else { var i, ins = "", numSpaces = instance.getOption("tabSize"); numSpaces -= to.ch % numSpaces; for (i = 0; i < numSpaces; i++) { ins += " "; } instance.replaceSelection(ins, "end"); } } } /** * @private * Handle left arrow, right arrow, backspace and delete keys when soft tabs are used. * @param {!CodeMirror} instance CodeMirror instance * @param {number} direction Direction of movement: 1 for forward, -1 for backward * @param {function} functionName name of the CodeMirror function to call * @return {boolean} true if key was handled */ function _handleSoftTabNavigation(instance, direction, functionName) { var handled = false; if (!instance.getOption("indentWithTabs")) { var cursor = instance.getCursor(), tabSize = instance.getOption("tabSize"), jump = cursor.ch % tabSize, line = instance.getLine(cursor.line); if (direction === 1) { jump = tabSize - jump; if (cursor.ch + jump > line.length) { // Jump would go beyond current line return false; } if (line.substr(cursor.ch, jump).search(/\S/) === -1) { instance[functionName](jump, "char"); handled = true; } } else { // Quick exit if we are at the beginning of the line if (cursor.ch === 0) { return false; } // If we are on the tab boundary, jump by the full amount, // but not beyond the start of the line. if (jump === 0) { jump = tabSize; } // Search backwards to the first non-space character var offset = line.substr(cursor.ch - jump, jump).search(/\s*$/g); if (offset !== -1) { // Adjust to jump to first non-space character jump -= offset; } if (jump > 0) { instance[functionName](-jump, "char"); handled = true; } } } return handled; } /** * Checks if the user just typed a closing brace/bracket/paren, and considers automatically * back-indenting it if so. */ function _checkElectricChars(jqEvent, editor, event) { var instance = editor._codeMirror; if (event.type === "keypress") { var keyStr = String.fromCharCode(event.which || event.keyCode); if (/[\]\{\}\)]/.test(keyStr)) { // If all text before the cursor is whitespace, auto-indent it var cursor = instance.getCursor(); var lineStr = instance.getLine(cursor.line); var nonWS = lineStr.search(/\S/); if (nonWS === -1 || nonWS >= cursor.ch) { // Need to do the auto-indent on a timeout to ensure // the keypress is handled before auto-indenting. // This is the same timeout value used by the // electricChars feature in CodeMirror. window.setTimeout(function () { instance.indentLine(cursor.line); }, 75); } } } } function _handleKeyEvents(jqEvent, editor, event) { _checkElectricChars(jqEvent, editor, event); // Pass the key event to the code hint manager. It may call preventDefault() on the event. CodeHintManager.handleKeyEvent(editor, event); } function _handleSelectAll() { var editor = EditorManager.getFocusedEditor(); if (editor) { editor._selectAllVisible(); } } /** * List of all current (non-destroy()ed) Editor instances. Needed when changing global preferences * that affect all editors, e.g. tabbing or color scheme settings. * @type {Array.<Editor>} */ var _instances = []; /** @type {boolean} Global setting: When inserting new text, use tab characters? (instead of spaces) */ var _useTabChar = false; /** * @constructor * * Creates a new CodeMirror editor instance bound to the given Document. The Document need not have * a "master" Editor realized yet, even if makeMasterEditor is false; in that case, the first time * an edit occurs we will automatically ask EditorManager to create a "master" editor to render the * Document modifiable. * * ALWAYS call destroy() when you are done with an Editor - otherwise it will leak a Document ref. * * @param {!Document} document * @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master" * Editor for the Document. If false, this Editor will attach to the Document as a "slave"/ * secondary editor. * @param {!string} mode Syntax-highlighting language mode; "" means plain-text mode. * See {@link EditorUtils#getModeFromFileExtension()}. * @param {!jQueryObject} container Container to add the editor to. * @param {!Object<string, function(Editor)>} additionalKeys Mapping of keyboard shortcuts to * custom handler functions. Mapping is in CodeMirror format * @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document * to display in this editor. Inclusive. */ function Editor(document, makeMasterEditor, mode, container, additionalKeys, range) { var self = this; _instances.push(this); // Attach to document: add ref & handlers this.document = document; document.addRef(); if (range) { // attach this first: want range updated before we process a change this._visibleRange = new TextRange(document, range.startLine, range.endLine); } // store this-bound version of listeners so we can remove them later this._handleDocumentChange = this._handleDocumentChange.bind(this); this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this); $(document).on("change", this._handleDocumentChange); $(document).on("deleted", this._handleDocumentDeleted); // (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized) this._inlineWidgets = []; // Editor supplies some standard keyboard behavior extensions of its own var codeMirrorKeyMap = { "Tab": _handleTabKey, "Shift-Tab": "indentLess", "Left": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "moveH")) { CodeMirror.commands.goCharLeft(instance); } }, "Right": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "moveH")) { CodeMirror.commands.goCharRight(instance); } }, "Backspace": function (instance) { if (!_handleSoftTabNavigation(instance, -1, "deleteH")) { CodeMirror.commands.delCharLeft(instance); } }, "Delete": function (instance) { if (!_handleSoftTabNavigation(instance, 1, "deleteH")) { CodeMirror.commands.delCharRight(instance); } }, "Esc": function (instance) { self.removeAllInlineWidgets(); }, "Shift-Delete": "cut", "Ctrl-Insert": "copy", "Shift-Insert": "paste" }; EditorManager.mergeExtraKeys(self, codeMirrorKeyMap, additionalKeys); // We'd like null/"" to mean plain text mode. CodeMirror defaults to plaintext for any // unrecognized mode, but it complains on the console in that fallback case: so, convert // here so we're always explicit, avoiding console noise. if (!mode) { mode = "text/plain"; } // Create the CodeMirror instance // (note: CodeMirror doesn't actually require using 'new', but jslint complains without it) this._codeMirror = new CodeMirror(container, { electricChars: false, // we use our own impl of this to avoid CodeMirror bugs; see _checkElectricChars() indentUnit: 4, indentWithTabs: _useTabChar, lineNumbers: true, matchBrackets: true, dragDrop: false, // work around issue #1123 extraKeys: codeMirrorKeyMap }); this._installEditorListeners(); $(this) .on("keyEvent", _handleKeyEvents) .on("change", this._handleEditorChange.bind(this)); // Set code-coloring mode BEFORE populating with text, to avoid a flash of uncolored text this._codeMirror.setOption("mode", mode); // Initially populate with text. This will send a spurious change event, so need to make // sure this is understood as a 'sync from document' case, not a genuine edit this._duringSync = true; this._resetText(document.getText()); this._duringSync = false; if (range) { // Hide all lines other than those we want to show. We do this rather than trimming the // text itself so that the editor still shows accurate line numbers. this._codeMirror.operation(function () { var i; for (i = 0; i < range.startLine; i++) { self._hideLine(i); } var lineCount = self.lineCount(); for (i = range.endLine + 1; i < lineCount; i++) { self._hideLine(i); } }); this.setCursorPos(range.startLine, 0); } // Now that we're fully initialized, we can point the document back at us if needed if (makeMasterEditor) { document._makeEditable(this); } // Add scrollTop property to this object for the scroll shadow code to use Object.defineProperty(this, "scrollTop", { get: function () { return this._codeMirror.getScrollInfo().y; } }); } /** * Removes this editor from the DOM and detaches from the Document. If this is the "master" * Editor that is secretly providing the Document's backing state, then the Document reverts to * a read-only string-backed mode. */ Editor.prototype.destroy = function () { // CodeMirror docs for getWrapperElement() say all you have to do is "Remove this from your // tree to delete an editor instance." $(this.getRootElement()).remove(); _instances.splice(_instances.indexOf(this), 1); // Disconnect from Document this.document.releaseRef(); $(this.document).off("change", this._handleDocumentChange); $(this.document).off("deleted", this._handleDocumentDeleted); if (this._visibleRange) { // TextRange also refs the Document this._visibleRange.dispose(); } // If we're the Document's master editor, disconnecting from it has special meaning if (this.document._masterEditor === this) { this.document._makeNonEditable(); } // Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks // run, at least, since they may also need to release Document refs this._inlineWidgets.forEach(function (inlineWidget) { inlineWidget.onClosed(); }); }; /** * Handles Select All specially when we have a visible range in order to work around * bugs in CodeMirror when lines are hidden. */ Editor.prototype._selectAllVisible = function () { var startLine = this.getFirstVisibleLine(), endLine = this.getLastVisibleLine(); this.setSelection({line: startLine, ch: 0}, {line: endLine, ch: this.document.getLine(endLine).length}); }; Editor.prototype._applyChanges = function (changeList) { var self = this; // _visibleRange has already updated via its own Document listener. See if this change caused // it to lose sync. If so, our whole view is stale - signal our owner to close us. if (this._visibleRange) { if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) { $(self).triggerHandler("lostContent"); return; } } // Apply text changes to CodeMirror editor var cm = this._codeMirror; cm.operation(function () { var change, newText; for (change = changeList; change; change = change.next) { newText = change.text.join('\n'); if (!change.from || !change.to) { if (change.from || change.to) { console.assert(false, "Change record received with only one end undefined--replacing entire text"); } cm.setValue(newText); } else { cm.replaceRange(newText, change.from, change.to); } } }); // The update above may have inserted new lines - must hide any that fall outside our range if (self._visibleRange) { cm.operation(function () { // TODO: could make this more efficient by only iterating across the min-max line // range of the union of all changes var i; for (i = 0; i < cm.lineCount(); i++) { if (i < self._visibleRange.startLine || i > self._visibleRange.endLine) { self._hideLine(i); } else { // Double-check that the set of NON-hidden lines matches our range too console.assert(!cm.getLineHandle(i).hidden); } } }); } }; /** * Responds to changes in the CodeMirror editor's text, syncing the changes to the Document. * There are several cases where we want to ignore a CodeMirror change: * - if we're the master editor, editor changes can be ignored because Document is already listening * for our changes * - if we're a secondary editor, editor changes should be ignored if they were caused by us reacting * to a Document change */ Editor.prototype._handleEditorChange = function (event, editor, changeList) { // we're currently syncing from the Document, so don't echo back TO the Document if (this._duringSync) { return; } // Secondary editor: force creation of "master" editor backing the model, if doesn't exist yet this.document._ensureMasterEditor(); if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; if we got here, this was a real editor change (not a // sync from the real ground truth), so we need to sync from us into the document // (which will directly push the change into the master editor). // FUTURE: Technically we should add a replaceRange() method to Document and go through // that instead of talking to its master editor directly. It's not clear yet exactly // what the right Document API would be, though. this._duringSync = true; this.document._masterEditor._applyChanges(changeList); this._duringSync = false; } // Else, Master editor: // we're the ground truth; nothing else to do, since Document listens directly to us // note: this change might have been a real edit made by the user, OR this might have // been a change synced from another editor if (this._visibleRange) { // _visibleRange has already updated via its own Document listener, when we pushed our // change into the Document above (_masterEditor._applyChanges()). But changes due to our // own edits should never cause the range to lose sync - verify that. if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) { throw new Error("ERROR: Typing in Editor should not destroy its own _visibleRange"); } } }; /** * Responds to changes in the Document's text, syncing the changes into our CodeMirror instance. * There are several cases where we want to ignore a Document change: * - if we're the master editor, Document changes should be ignored becuase we already have the right * text (either the change originated with us, or it has already been set into us by Document) * - if we're a secondary editor, Document changes should be ignored if they were caused by us sending * the document an editor change that originated with us */ Editor.prototype._handleDocumentChange = function (event, doc, changeList) { var change; // we're currently syncing to the Document, so don't echo back FROM the Document if (this._duringSync) { return; } if (this.document._masterEditor !== this) { // Secondary editor: // we're not the ground truth; and if we got here, this was a Document change that // didn't come from us (e.g. a sync from another editor, a direct programmatic change // to the document, or a sync from external disk changes)... so sync from the Document this._duringSync = true; this._applyChanges(changeList); this._duringSync = false; } // Else, Master editor: // we're the ground truth; nothing to do since Document change is just echoing our // editor changes }; /** * Responds to the Document's underlying file being deleted. The Document is now basically dead, * so we must close. */ Editor.prototype._handleDocumentDeleted = function () { $(this).triggerHandler("lostContent"); }; /** * Install singleton event handlers on the CodeMirror instance, translating them into multi- * listener-capable jQuery events on the Editor instance. */ Editor.prototype._installEditorListeners = function () { var self = this; // FUTURE: if this list grows longer, consider making this a more generic mapping // NOTE: change is a "private" event--others shouldn't listen to it on Editor, only on // Document this._codeMirror.setOption("onChange", function (instance, changeList) { $(self).triggerHandler("change", [self, changeList]); }); this._codeMirror.setOption("onKeyEvent", function (instance, event) { $(self).triggerHandler("keyEvent", [self, event]); return event.defaultPrevented; // false tells CodeMirror we didn't eat the event }); this._codeMirror.setOption("onCursorActivity", function (instance) { $(self).triggerHandler("cursorActivity", [self]); }); this._codeMirror.setOption("onScroll", function (instance) { // close all dropdowns on scroll Menus.closeAll(); $(self).triggerHandler("scroll", [self]); // notify all inline widgets of a position change self._fireWidgetOffsetTopChanged(self.getFirstVisibleLine() - 1); }); }; /** * Sets the contents of the editor and clears the undo/redo history. Dispatches a change event. * Semi-private: only Document should call this. * @param {!string} text */ Editor.prototype._resetText = function (text) { var perfTimerName = PerfUtils.markStart("Edtitor._resetText()\t" + (!this.document || this.document.file.fullPath)); var cursorPos = this.getCursorPos(), scrollPos = this.getScrollPos(); // This *will* fire a change event, but we clear the undo immediately afterward this._codeMirror.setValue(text); // Make sure we can't undo back to the empty state before setValue() this._codeMirror.clearHistory(); // restore cursor and scroll positions this.setCursorPos(cursorPos); this.setScrollPos(scrollPos.x, scrollPos.y); PerfUtils.addMeasurement(perfTimerName); }; /** * Gets the current cursor position within the editor. If there is a selection, returns whichever * end of the range the cursor lies at. * @return !{line:number, ch:number} */ Editor.prototype.getCursorPos = function () { return this._codeMirror.getCursor(); }; /** * Sets the cursor position within the editor. Removes any selection. * @param {number} line The 0 b