diff --git a/src/extensions/bramble-extensions.json b/src/extensions/bramble-extensions.json index 3f1f079ae60..0a870230274 100644 --- a/src/extensions/bramble-extensions.json +++ b/src/extensions/bramble-extensions.json @@ -51,6 +51,18 @@ "extensions/default/InlineColorEditor/img/*.png" ] }, + { + "path": "extensions/default/InlineBorderRadiusEditor", + "less": { + "dist/extensions/default/InlineBorderRadiusEditor/css/main.css": [ + "src/extensions/default/InlineBorderRadiusEditor/css/main.less" + ] + }, + "copy": [ + "extensions/default/InlineBorderRadiusEditor/img/*.png", + "extensions/default/InlineBorderRadiusEditor/BorderRadiusEditorTemplate.html" + ] + }, { "path": "extensions/default/Inline3DParametersEditor", "less": { diff --git a/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditor.js b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditor.js new file mode 100644 index 00000000000..4628fa861b2 --- /dev/null +++ b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditor.js @@ -0,0 +1,225 @@ +define(function(require, exports, module) { + "use strict"; + + var Strings = brackets.getModule("strings"); + var Mustache = brackets.getModule("thirdparty/mustache/mustache"); + var BorderRadiusUtils = require("BorderRadiusUtils"); + + // getting reference to the html template for the border-radius editor UI + var BorderRadiusTemplate = require("text!BorderRadiusEditorTemplate.html"); + + function getIndividualValues(values){ + // Convert "12px 20px 30px" into an array of individual values like: + // [{num: 12, unit: "px"}, {num: 20, unit: "px"}, ...] + var individualValues = []; + var currentValue; + + // We create a new regular expression everytime so that we don't + // reuse stale data from the old RegExp object. + var valueRegex = new RegExp(BorderRadiusUtils.BORDER_RADIUS_SINGLE_VALUE_REGEX); + + while ((currentValue = valueRegex.exec(values)) !== null) { + individualValues.push({ + num: parseFloat(currentValue[1]), + unit: currentValue[2] || "" + }); + } + + return individualValues; + } + + function BorderRadiusValue($parentElement, location, value, unit, onChange) { + var self = this; + + self.value = value || 0; + self.unit = unit || ""; + + var $slider = this.$slider = $parentElement.find("#" + location + "-slider"); + var $unitOptions = $parentElement.find("#" + location + "-radio").children(); + var $text = $parentElement.find("#" + location + "-text"); + + $slider.val(self.value); + $unitOptions.filter(function() { + return $(this).text().trim() === self.unit; + }).addClass("selected"); + $text.text(self.toString()); + + $slider.on("input", function() { + var newValue = $slider.val().trim(); + self.value = newValue; + $text.text(self.toString()); + onChange(); + }); + + $unitOptions.on("click", function() { + var $selectedUnit = $(this); + $selectedUnit.siblings().removeClass("selected"); + $selectedUnit.addClass("selected"); + + self.unit = $selectedUnit.text().trim(); + $text.text(self.toString()); + onChange(); + }); + } + + BorderRadiusValue.prototype.toString = function() { + return this.value + (this.value === 0 ? "" : this.unit); + }; + + function BorderRadiusEditor($parent, valueString, radiusChangeHandler) { + var self = this; + + // Create the DOM structure, filling in localized strings via Mustache + self.$element = $(Mustache.render(BorderRadiusTemplate, Strings)); + $parent.append(self.$element); + self.radiusChangeHandler = radiusChangeHandler; + + this.onChange = this.onChange.bind(this); + self.updateValues(valueString); + + // Attach event listeners to toggle the corner mode UI elements + var $individualCornerArea = self.$element.find("#individualCornerArea"); + var $individualCornerButton = self.$element.find("#individualCorners"); + var $allCornersArea = self.$element.find("#allCornersArea"); + var $allCornerButton = self.$element.find("#allCorners"); + + function toggleCornerOption($showElement, $hideElement) { + $showElement.show(); + $hideElement.hide(); + self.allCorners = $showElement === $allCornersArea; + self.onChange(); + } + + $allCornerButton.on("click", function() { + $allCornerButton.addClass("selected"); + $individualCornerButton.removeClass("selected"); + toggleCornerOption($allCornersArea, $individualCornerArea); + }); + $individualCornerButton.on("click", function() { + $allCornerButton.removeClass("selected"); + $individualCornerButton.addClass("selected"); + toggleCornerOption($individualCornerArea, $allCornersArea); + }); + + // initialize individual corner editing to be disabled if allCorner is set to true + if(self.allCorners){ + $allCornerButton.trigger("click"); + } else { + $individualCornerButton.trigger("click"); + } + } + + BorderRadiusEditor.prototype.updateValues = function(valueString) { + var values = getIndividualValues(valueString); + var numOfValues = values.length; + var firstValue = values[0]; + var secondValue = firstValue; + var thirdValue = firstValue; + var fourthValue = firstValue; + + this.allCorners = values.length === 1; + + if (!this.allCorners) { + secondValue = values[1]; + + if (numOfValues === 2) { + fourthValue = secondValue; + } else { + thirdValue = values[2]; + + if (numOfValues === 3) { + fourthValue = secondValue; + } else { + fourthValue = values[3]; + } + } + } + + this.topLeft = new BorderRadiusValue( + this.$element, + "top-left", + firstValue.num, + firstValue.unit, + this.onChange + ); + this.topRight = new BorderRadiusValue( + this.$element, + "top-right", + secondValue.num, + secondValue.unit, + this.onChange + ); + this.bottomRight = new BorderRadiusValue( + this.$element, + "bottom-right", + thirdValue.num, + thirdValue.unit, + this.onChange + ); + this.bottomLeft = new BorderRadiusValue( + this.$element, + "bottom-left", + fourthValue.num, + fourthValue.unit, + this.onChange + ); + this.allCorner = new BorderRadiusValue( + this.$element, + "all-corner", + firstValue.num, + firstValue.unit, + this.onChange + ); + + //correctly update the values in the UI. + this.onChange(); + }; + + BorderRadiusEditor.prototype.onChange = function() { + if (this.allCorners) { + this.radiusChangeHandler(this.allCorner.toString()); + return; + } + + var topLeft = this.topLeft.toString(); + var topRight = this.topRight.toString(); + var bottomRight = this.bottomRight.toString(); + var bottomLeft = this.bottomLeft.toString(); + var borderRadiusString; + + if (topRight === bottomLeft) { + // We can use a two value border radius if top right and + // bottom left are equal and top left and bottom right are equal. + // For e.g. 20px 30px + borderRadiusString = topLeft + " " + topRight; + + if (topLeft !== bottomRight) { + // We can use a three value border radius if top right and + // bottom left are equal but the top left and bottom right + // values are not. + borderRadiusString += " " + bottomRight; + } else if (topLeft === topRight) { + // This means that top left and bottom right are equal (set 1), + // the top right and bottom left values are equal (set 2), and + // that set 1 and set 2 are equal - implying that all values + // are the same. + borderRadiusString = topLeft; + } + } else { + borderRadiusString = [topLeft, topRight, bottomRight, bottomLeft].join(" "); + } + + this.radiusChangeHandler(borderRadiusString); + }; + + BorderRadiusEditor.prototype.focus = function() { + this.topLeft.$slider.focus(); + }; + + BorderRadiusEditor.prototype.isValidBorderRadiusString = function(string){ + var radiusValueRegEx = new RegExp(BorderRadiusUtils.BORDER_RADIUS_VALUE_REGEX); + return radiusValueRegEx.test(string); + }; + + exports.BorderRadiusEditor = BorderRadiusEditor; +}); diff --git a/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditorTemplate.html b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditorTemplate.html new file mode 100644 index 00000000000..da1f0e22f52 --- /dev/null +++ b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusEditorTemplate.html @@ -0,0 +1,121 @@ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + +
+
+
+ + + + + +
+
+
+ + + + + +
+
+
+ + + + + +
+
+ +
+ + + + + + + +
+
+
+
+
+
+
+
+ + + + + +
+
+
diff --git a/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusProperties.json b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusProperties.json new file mode 100644 index 00000000000..89b8bda0543 --- /dev/null +++ b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusProperties.json @@ -0,0 +1,7 @@ +{ + "border-radius": {"values": ["inherit"]}, + "border-top-left-radius": {"values": ["inherit"]}, + "border-top-right-radius": {"values": ["inherit"]}, + "border-bottom-left-radius": {"values": ["inherit"]}, + "border-bottom-right-radius": {"values": ["inherit"]} +} diff --git a/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusUtils.js b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusUtils.js new file mode 100644 index 00000000000..ef39198b656 --- /dev/null +++ b/src/extensions/default/InlineBorderRadiusEditor/BorderRadiusUtils.js @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2013 - 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. + * + */ + +/** + * Utilities regular expressions related to border-radius rule matching + * + */ +define(function (require, exports, module) { + "use strict"; + /** + * Regular expression that matches the css rule for border-radius values after the + * colon is optional + * @const @type {RegExp} + */ + var BORDER_RADIUS_REGEX = new RegExp('.*border-radius:.*'); + + /** + * Regular expression that matches the reasonable format of css value for border-radius, + * starting with a number or decimal followed by any scalable units listed in the + * expression. Such pattern may occur up to 4 times since maximum of 4 corners can be used. + * We use a regex as detailed below: + * (\d+\.?\d*) matches a decimal or integer number + * (px|em|%)? matches the unit which is optional (mainly for 0) + * {1,4} makes sure that the above two groups together are only present + * between 1 to 4 times (both inclusive). + * @const @type {RegExp} + */ + var BORDER_RADIUS_VALUE_REGEX = new RegExp(/((\d+\.?\d*)(px|em|%)?){1,4}.*/); + // Matches a single value and captures the number and unit. Use it with exec() + // to find successive values in a valid border radius value string. + var BORDER_RADIUS_SINGLE_VALUE_REGEX = new RegExp(/(\d+\.?\d*)(px|em|%)?/, "g"); + + // Define public API + exports.BORDER_RADIUS_REGEX = BORDER_RADIUS_REGEX; + exports.BORDER_RADIUS_VALUE_REGEX = BORDER_RADIUS_VALUE_REGEX; + exports.BORDER_RADIUS_SINGLE_VALUE_REGEX = BORDER_RADIUS_SINGLE_VALUE_REGEX; +}); diff --git a/src/extensions/default/InlineBorderRadiusEditor/InlineBorderRadiusEditor.js b/src/extensions/default/InlineBorderRadiusEditor/InlineBorderRadiusEditor.js new file mode 100644 index 00000000000..a7665d5045f --- /dev/null +++ b/src/extensions/default/InlineBorderRadiusEditor/InlineBorderRadiusEditor.js @@ -0,0 +1,230 @@ +/* + * 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"; + + var InlineWidget = brackets.getModule("editor/InlineWidget").InlineWidget; + var BorderRadiusUtils = require("BorderRadiusUtils"); + var BorderRadiusEditor = require("BorderRadiusEditor").BorderRadiusEditor; + + + /** @const @type {number} */ + var DEFAULT_BORDER_RADIUS = "30px"; + + /** @type {number} Global var used to provide a unique ID for each borderRadius editor instance's _origin field. */ + var lastOriginId = 1; + + /** + * Inline widget containing a BorderRadiusEditor control + * @param {!string} borderRadius Initially selected borderRadius + * @param {!CodeMirror.TextMarker} marker + */ + function InlineBorderRadiusEditor(borderRadius, marker) { + this._borderRadius = borderRadius; + this._marker = marker; + this._isOwnChange = false; + this._isHostChange = false; + this._origin = "+InlineBorderRadiusEditor_" + (lastOriginId++); + + this._handleBorderRadiusChange = this._handleBorderRadiusChange.bind(this); + this._handleHostDocumentChange = this._handleHostDocumentChange.bind(this); + + InlineWidget.call(this); + } + + InlineBorderRadiusEditor.prototype = Object.create(InlineWidget.prototype); + InlineBorderRadiusEditor.prototype.constructor = InlineBorderRadiusEditor; + InlineBorderRadiusEditor.prototype.parentClass = InlineWidget.prototype; + + /** @type {!borderRaidusEditor} borderRadiusEditor instance */ + InlineBorderRadiusEditor.prototype.borderRaidusEditor = null; + + /** + * Range of code we're attached to; _marker.find() may by null if sync is lost. + * @type {!CodeMirror.TextMarker} + */ + InlineBorderRadiusEditor.prototype._marker = null; + + /** @type {boolean} True while we're syncing a borderRadiusEditor change into the code editor */ + InlineBorderRadiusEditor.prototype._isOwnChange = null; + + /** @type {boolean} True while we're syncing a code editor change into the borderRadiusEditor*/ + InlineBorderRadiusEditor.prototype._isHostChange = null; + + /** @type {number} ID used to identify edits coming from this inline widget for undo batching */ + InlineBorderRadiusEditor.prototype._origin = null; + + + /** + * Returns the current text range of the border-radius value we're attached to, or null if + * we've lost sync with what's in the code. + * @return {?{start:{line:number, ch:number}, end:{line:number, ch:number}}} + */ + InlineBorderRadiusEditor.prototype.getCurrentRange = function () { + var pos, start, end; + + pos = this._marker && this._marker.find(); + + start = pos && pos.from; + if (!start) { + return null; + } + + end = pos.to; + if (!end) { + end = {line: start.line}; + } + + // Even if we think we have a good range end, we want to run the + // regexp match to see if there's a valid match that extends past the marker. + // This can happen if the user deletes the end of the existing border-radius value and then + // types some more. + + //Manuelly find the position of the first occurance of radius value in the line + //because using this._maker.find() does not return expected value + //using this as a work around + var line = this.hostEditor.document.getLine(start.line); + for(var i = line.indexOf(":")+1; i end)); + + if(match){ + // Check if the cursorLine has a CSS rule of type border-radius + var cssPropertyName, semiColonPos, colonPos, radiusValue, cursorLineSubstring, firstCharacterPos; + + // Get the css property name after removing spaces and ":" so that we can check for it in the file BorderRadiusProperties.json + cssPropertyName = cursorLine.split(':')[0].trim(); + + if (!cssPropertyName || !properties[cssPropertyName]) { + return null; + } + + if (properties[cssPropertyName]) { + colonPos = cursorLine.indexOf(":"); + semiColonPos = cursorLine.indexOf(";"); + cursorLineSubstring = cursorLine.substring(colonPos + 1, cursorLine.length); + radiusValue = cursorLineSubstring.replace(/ /g,"").replace(";", ""); + if (radiusValue) { + if (radiusValueRegEx.test(radiusValue)) { + // edit the radius value of an existing css rule + firstCharacterPos = cursorLineSubstring.search(/\S/); + pos.ch = colonPos + 1 + Math.min(firstCharacterPos,1); + if (semiColonPos !== -1) { + endPos = {line: pos.line, ch: semiColonPos}; + } else { + endPos = {line: pos.line, ch: cursorLine.length}; + } + } else { + return null; + } + } else { + // edit the radius value of a new css rule + var newText = " ", from, to; + newText = newText.concat(DEFAULT_RADIUS, ";"); + from = {line: pos.line, ch: colonPos + 1}; + to = {line: pos.line, ch: cursorLine.length}; + hostEditor._codeMirror.replaceRange(newText, from, to); + pos.ch = colonPos + 2; + endPos = {line: pos.line, ch: pos.ch + DEFAULT_RADIUS.length}; + radiusValue = DEFAULT_RADIUS; + } + + marker = hostEditor._codeMirror.markText(pos, endPos); + hostEditor.setSelection(pos, endPos); + + return { + radius: radiusValue, + marker: marker + }; + } + } + return null; + // Adjust pos to the beginning of the match so that the inline editor won't get + // dismissed while we're updating the border-radius with the new values from user's inline editing + } + + /** + * Registered as an inline editor provider: creates an InlineBorderRadiusEditor when the cursor + * is on a border-radius value (in any flavor of code). + * + * @param {!Editor} hostEditor + * @param {!{line:Number, ch:Number}} pos + * @return {?$.Promise} synchronously resolved with an InlineWidget, or null if there's + * no border-radius at pos. + */ + function inlineBorderRadiusEditorProvider(hostEditor, pos) { + var context = prepareEditorForProvider(hostEditor, pos), + inlineBorderRadiusEditor, + result; + + if (!context) { + return null; + } else { + inlineBorderRadiusEditor = new InlineBorderRadiusEditor(context.radius, context.marker); + inlineBorderRadiusEditor.load(hostEditor); + + result = new $.Deferred(); + result.resolve(inlineBorderRadiusEditor); + return result.promise(); + } + } + + function queryInlineBorderRadiusEditorProvider(hostEditor, pos) { + var borderRadiusRegEx, cursorLine, match, sel, start, end, endPos, marker; + var cssPropertyName, semiColonPos, colonPos, borderRadiusValue, cursorLineSubstring, firstCharacterPos; + + sel = hostEditor.getSelection(); + if (sel.start.line !== sel.end.line) { + return false; + } + + borderRadiusRegEx = new RegExp(BorderRadiusUtils.BORDER_RADIUS_REGEX); + cursorLine = hostEditor.document.getLine(pos.line); + + // Loop through each match of borderRadiusRegEx and stop when the one that contains pos is found. + do { + match = borderRadiusRegEx.exec(cursorLine); + if (match) { + start = match.index; + end = start + match[0].length; + } + } while (match && (pos.ch < start || pos.ch > end)); + + if (match) { + return true; + } + + // Get the css property name after removing spaces and ":" so that we can check for it in the file BorderRadiusProperties.json + cssPropertyName = cursorLine.split(':')[0].trim(); + + if (!cssPropertyName || !properties[cssPropertyName]) { + return false; + } + + if (properties[cssPropertyName]) { + colonPos = cursorLine.indexOf(":"); + semiColonPos = cursorLine.indexOf(";"); + cursorLineSubstring = cursorLine.substring(colonPos + 1, cursorLine.length); + borderRadiusValue = cursorLineSubstring.replace(/ /g,"").replace(";", ""); + if (borderRadiusValue) { + return borderRadiusRegEx.test(borderRadiusValue); + } + return true; + } + + return false; + } + + // Initialize extension + ExtensionUtils.loadStyleSheet(module, "css/main.less"); + EditorManager.registerInlineEditProvider(inlineBorderRadiusEditorProvider, queryInlineBorderRadiusEditorProvider); + exports.prepareEditorForProvider = prepareEditorForProvider; +});