/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/editor/CSSInlineEditor.js
379 строк
16 KB
Sorab Bisht
Adding Analytics logging for JSRefactor, Live Preview, Quick Edit and more features (#14253)
07 май 2018, 11:37
07 май 2018, 11:37
e0b6620
Код
Авторство
О чём код?
/* * 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. * */ define(function (require, exports, module) { "use strict"; // Load dependent modules var CSSUtils = require("language/CSSUtils"), DropdownButton = require("widgets/DropdownButton").DropdownButton, CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), Editor = require("editor/Editor").Editor, LanguageManager = require("language/LanguageManager"), ProjectManager = require("project/ProjectManager"), FileUtils = require("file/FileUtils"), HTMLUtils = require("language/HTMLUtils"), MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor"), Strings = require("strings"), ViewUtils = require("utils/ViewUtils"), HealthLogger = require("utils/HealthLogger"), _ = require("thirdparty/lodash"); var _newRuleCmd, _newRuleHandlers = []; function _getCSSFilesInProject() { return ProjectManager.getAllFiles(ProjectManager.getLanguageFilter(["css", "less", "scss"])); } /** * 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 * @param {!{line:Number, ch:Number}} pos * @return {selectorName: {string}, reason: {string}} * @private */ function _getSelectorName(editor, pos) { var tagInfo = HTMLUtils.getTagInfo(editor, pos), selectorName = "", reason; if (tagInfo.position.tokenType === HTMLUtils.TAG_NAME || tagInfo.position.tokenType === HTMLUtils.CLOSING_TAG) { // 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; if (/\S/.test(attributeValue)) { 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 between two classnames, selectorName is "." if (selectorName === ".") { selectorName = ""; reason = Strings.ERROR_CSSQUICKEDIT_BETWEENCLASSES; } } else { reason = Strings.ERROR_CSSQUICKEDIT_CLASSNOTFOUND; } } else if (tagInfo.attr.name === "id") { // ID selector var trimmedVal = tagInfo.attr.value.trim(); if (trimmedVal) { selectorName = "#" + trimmedVal; } else { reason = Strings.ERROR_CSSQUICKEDIT_IDNOTFOUND; } } else { reason = Strings.ERROR_CSSQUICKEDIT_UNSUPPORTEDATTR; } } return { selectorName: selectorName, reason: reason }; } /** * @private * Add a new rule for the given selector to the given stylesheet, then add the rule to the * given inline editor. * @param {string} selectorName The selector to create a rule for. * @param {MultiRangeInlineEditor} inlineEditor The inline editor to display the new rule in. * @param {string} path The path to the stylesheet file. */ function _addRule(selectorName, inlineEditor, path) { DocumentManager.getDocumentForPath(path).done(function (styleDoc) { var newRuleInfo = CSSUtils.addRuleToDocument(styleDoc, selectorName, Editor.getUseTabChar(path), Editor.getSpaceUnits(path)); inlineEditor.addAndSelectRange(selectorName, styleDoc, newRuleInfo.range.from.line, newRuleInfo.range.to.line); inlineEditor.editor.setCursorPos(newRuleInfo.pos.line, newRuleInfo.pos.ch); }); } /** * @private * Handle the "new rule" menu item by dispatching it to the handler for the focused inline editor. */ function _handleNewRule() { var inlineEditor = MultiRangeInlineEditor.getFocusedMultiRangeInlineEditor(); if (inlineEditor) { var handlerInfo = _.find(_newRuleHandlers, function (entry) { return entry.inlineEditor === inlineEditor; }); if (handlerInfo) { handlerInfo.handler(); } } } /** Item renderer for stylesheet-picker dropdown */ function _stylesheetListRenderer(item) { var html = "<span class='stylesheet-name'>" + _.escape(item.name); if (item.subDirStr) { html += "<span class='stylesheet-dir'> — " + _.escape(item.subDirStr) + "</span>"; } html += "</span>"; return html; } /** * 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} synchronously resolved with an InlineWidget; or error * {string} if pos is in tag but not in tag name, class attr, or id attr; or null if the * selection isn't even close to a context where we could provide anything. */ function htmlToCSSProvider(hostEditor, pos) { // Only provide a CSS editor when cursor is in HTML content if (hostEditor.getLanguageForSelection().getId() !== "html") { return null; } //Send analytics data for QuickEdit open HealthLogger.sendAnalyticsData( "QuickEditOpen", "usage", "quickEdit", "open" ); // Only provide CSS editor if the selection is within a single line var sel = hostEditor.getSelection(); 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 selectorResult = _getSelectorName(hostEditor, sel.start); if (selectorResult.selectorName === "") { return selectorResult.reason || null; } var selectorName = selectorResult.selectorName; var result = new $.Deferred(), cssInlineEditor, cssFileInfos = [], newRuleButton; /** * @private * Callback when item from dropdown list is selected */ function _onDropdownSelect(event, fileInfo) { _addRule(selectorName, cssInlineEditor, fileInfo.fullPath); } /** * @private * Checks to see if there are any stylesheets in the project, and returns the appropriate * "no rules"/"no stylesheets" message accordingly. * @return {$.Promise} a promise that is resolved with the message to show. Never rejected. */ function _getNoRulesMsg() { var result = new $.Deferred(); _getCSSFilesInProject().done(function (fileInfos) { result.resolve(fileInfos.length ? Strings.CSS_QUICK_EDIT_NO_MATCHES : Strings.CSS_QUICK_EDIT_NO_STYLESHEETS); }); return result; } /** * @private * Update the enablement of associated menu commands. */ function _updateCommands() { _newRuleCmd.setEnabled(cssInlineEditor.hasFocus() && !newRuleButton.$button.hasClass("disabled")); } /** * @private * Create a new rule on click. */ function _handleNewRuleClick(e) { if (!newRuleButton.$button.hasClass("disabled")) { if (cssFileInfos.length === 1) { // Just go ahead and create the rule. _addRule(selectorName, cssInlineEditor, cssFileInfos[0].fullPath); } else { // Although not attached to button click in 'dropdown mode', this handler can still be // invoked via the command shortcut. Just toggle dropdown open/closed in that case. newRuleButton.toggleDropdown(); } } } /** * @private * Sort files with LESS/SCSS above CSS, and then within each grouping sort by path & filename * (the same order we use for Find in Files) * @param {!File} a, b * @return {number} */ function _fileComparator(a, b) { var aIsCSS = LanguageManager.getLanguageForPath(a.fullPath).getId() === "css", bIsCSS = LanguageManager.getLanguageForPath(b.fullPath).getId() === "css"; if (aIsCSS && !bIsCSS) { return 1; } else if (!aIsCSS && bIsCSS) { return -1; } else { return FileUtils.comparePaths(a.fullPath, b.fullPath); } } /** * @private * Prepare file list for display */ function _prepFileList(files) { // First, sort list (the same ordering we use for the results list) files.sort(_fileComparator); // Find any files that share the same name (with different path) var fileNames = {}; files.forEach(function (file) { if (!fileNames[file.name]) { fileNames[file.name] = []; } fileNames[file.name].push(file); }); // For any duplicate filenames, set subDirStr to a path snippet the helps // the user distinguish each file in the list. _.forEach(fileNames, function (files) { if (files.length > 1) { var displayPaths = ViewUtils.getDirNamesForDuplicateFiles(files); files.forEach(function (file, i) { file.subDirStr = displayPaths[i]; }); } }); return files; } function _onHostEditorScroll() { newRuleButton.closeDropdown(); } CSSUtils.findMatchingRules(selectorName, hostEditor.document) .done(function (rules) { var inlineEditorDeferred = new $.Deferred(); cssInlineEditor = new MultiRangeInlineEditor.MultiRangeInlineEditor(CSSUtils.consolidateRules(rules), _getNoRulesMsg, CSSUtils.getRangeSelectors, _fileComparator); cssInlineEditor.load(hostEditor); cssInlineEditor.$htmlContent .on("focusin", _updateCommands) .on("focusout", _updateCommands); cssInlineEditor.on("add", function () { inlineEditorDeferred.resolve(); }); cssInlineEditor.on("close", function () { newRuleButton.closeDropdown(); hostEditor.off("scroll", _onHostEditorScroll); }); var $header = $(".inline-editor-header", cssInlineEditor.$htmlContent); newRuleButton = new DropdownButton(Strings.BUTTON_NEW_RULE, [], _stylesheetListRenderer); // actual item list populated later, below newRuleButton.$button.addClass("disabled"); // disabled until list is known newRuleButton.$button.addClass("btn-mini stylesheet-button"); $header.append(newRuleButton.$button); _newRuleHandlers.push({inlineEditor: cssInlineEditor, handler: _handleNewRuleClick}); hostEditor.on("scroll", _onHostEditorScroll); result.resolve(cssInlineEditor); // Now that dialog has been built, collect list of stylesheets var stylesheetsPromise = _getCSSFilesInProject(); // After both the stylesheets are loaded and the inline editor has been added to the DOM, // update the UI accordingly. (Those can happen in either order, so we need to wait for both.) // Note that the stylesheetsPromise needs to be passed first in order for the fileInfos to be // properly passed to the handler, since $.when() passes the results in order of the argument // list. $.when(stylesheetsPromise, inlineEditorDeferred.promise()) .done(function (fileInfos) { cssFileInfos = _prepFileList(fileInfos); // "New Rule" button is disabled by default and gets enabled // here if there are any stylesheets in project if (cssFileInfos.length > 0) { newRuleButton.$button.removeClass("disabled"); if (!rules.length) { // Force focus to the button so the user can create a new rule from the keyboard. newRuleButton.$button.focus(); } if (cssFileInfos.length === 1) { // Make it look & feel like a plain button in this case newRuleButton.$button.removeClass("btn-dropdown"); newRuleButton.$button.on("click", _handleNewRuleClick); } else { // Fill out remaining dropdown attributes otherwise newRuleButton.items = cssFileInfos; newRuleButton.on("select", _onDropdownSelect); } } _updateCommands(); }); }) .fail(function (error) { console.warn("Error in findMatchingRules()", error); result.reject(); }); return result.promise(); } EditorManager.registerInlineEditProvider(htmlToCSSProvider); _newRuleCmd = CommandManager.register(Strings.CMD_CSS_QUICK_EDIT_NEW_RULE, Commands.CSS_QUICK_EDIT_NEW_RULE, _handleNewRule); _newRuleCmd.setEnabled(false); });