var _editHandler;
var HIGHLIGHT_CLASSNAME = "__brackets-ld-highlight",
KEEP_ALIVE_TIMEOUT = 3000; // Keep alive timeout value, in milliseconds
// determine whether an event should be processed for Live Development
function _validEvent(event) {
if (window.navigator.platform.substr(0, 3) === "Mac") {
// Mac
return event.metaKey;
} else {
// Windows
return event.ctrlKey;
}
}
// determine the color for a type
function _typeColor(type, highlight) {
switch (type) {
case "html":
return highlight ? "#eec" : "#ffe";
case "css":
return highlight ? "#cee" : "#eff";
case "js":
return highlight ? "#ccf" : "#eef";
default:
return highlight ? "#ddd" : "#eee";
}
}
// compute the screen offset of an element
function _screenOffset(element) {
var elemBounds = element.getBoundingClientRect(),
body = window.document.body,
offsetTop,
offsetLeft;
if (window.getComputedStyle(body).position === "static") {
offsetLeft = elemBounds.left + window.pageXOffset;
offsetTop = elemBounds.top + window.pageYOffset;
} else {
var bodyBounds = body.getBoundingClientRect();
offsetLeft = elemBounds.left - bodyBounds.left;
offsetTop = elemBounds.top - bodyBounds.top;
}
return { left: offsetLeft, top: offsetTop };
}
// set an event on a element
function _trigger(element, name, value, autoRemove) {
var key = "data-ld-" + name;
if (value !== undefined && value !== null) {
element.setAttribute(key, value);
if (autoRemove) {
window.setTimeout(element.removeAttribute.bind(element, key));
}
} else {
element.removeAttribute(key);
}
}
// Checks if the element is in Viewport in the client browser
function isInViewport(element) {
var rect = element.getBoundingClientRect();
var html = window.document.documentElement;
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || html.clientHeight) &&
rect.right <= (window.innerWidth || html.clientWidth)
);
}
// returns the distance from the top of the closest relatively positioned parent element
function getDocumentOffsetTop(element) {
return element.offsetTop + (element.offsetParent ? getDocumentOffsetTop(element.offsetParent) : 0);
}
// construct the info menu
function Menu(element) {
this.element = element;
_trigger(this.element, "showgoto", 1, true);
window.setTimeout(window.remoteShowGoto);
this.remove = this.remove.bind(this);
}
Menu.prototype = {
onClick: function (url, event) {
event.preventDefault();
_trigger(this.element, "goto", url, true);
this.remove();
},
createBody: function () {
if (this.body) {
return;
}
// compute the position on screen
var offset = _screenOffset(this.element),
x = offset.left,
y = offset.top + this.element.offsetHeight;
// create the container
this.body = window.document.createElement("div");
this.body.style.setProperty("z-index", 2147483647);
this.body.style.setProperty("position", "absolute");
this.body.style.setProperty("left", x + "px");
this.body.style.setProperty("top", y + "px");
this.body.style.setProperty("font-size", "11pt");
// draw the background
this.body.style.setProperty("background", "#fff");
this.body.style.setProperty("border", "1px solid #888");
this.body.style.setProperty("-webkit-box-shadow", "2px 2px 6px 0px #ccc");
this.body.style.setProperty("border-radius", "6px");
this.body.style.setProperty("padding", "6px");
},
addItem: function (target) {
var item = window.document.createElement("div");
item.style.setProperty("padding", "2px 6px");
if (this.body.childNodes.length > 0) {
item.style.setProperty("border-top", "1px solid #ccc");
}
item.style.setProperty("cursor", "pointer");
item.style.setProperty("background", _typeColor(target.type));
item.innerHTML = target.name;
item.addEventListener("click", this.onClick.bind(this, target.url));
if (target.file) {
var file = window.document.createElement("i");
file.style.setProperty("float", "right");
file.style.setProperty("margin-left", "12px");
file.innerHTML = " " + target.file;
item.appendChild(file);
}
this.body.appendChild(item);
},
show: function () {
if (!this.body) {
this.body = this.createBody();
}
if (!this.body.parentNode) {
window.document.body.appendChild(this.body);
}
window.document.addEventListener("click", this.remove);
},
remove: function () {
if (this.body && this.body.parentNode) {
window.document.body.removeChild(this.body);
}
window.document.removeEventListener("click", this.remove);
}
};
function Editor(element) {
this.onBlur = this.onBlur.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.element = element;
this.element.setAttribute("contenteditable", "true");
this.element.focus();
this.element.addEventListener("blur", this.onBlur);
this.element.addEventListener("keypress", this.onKeyPress);
this.revertText = this.element.innerHTML;
_trigger(this.element, "edit", 1);
}
Editor.prototype = {
onBlur: function (event) {
this.element.removeAttribute("contenteditable");
this.element.removeEventListener("blur", this.onBlur);
this.element.removeEventListener("keypress", this.onKeyPress);
_trigger(this.element, "edit", 0, true);
},
onKeyPress: function (event) {
switch (event.which) {
case 13: // return
this.element.blur();
break;
case 27: // esc
this.element.innerHTML = this.revertText;
this.element.blur();
break;
}
}
};
function Highlight(color, trigger) {
this.color = color;
this.trigger = !!trigger;
this.elements = [];
this.selector = "";
}
Highlight.prototype = {
_elementExists: function (element) {
var i;
for (i in this.elements) {
if (this.elements[i] === element) {
return true;
}
}
return false;
},
_makeHighlightDiv: function (element, doAnimation) {
var elementBounds = element.getBoundingClientRect(),
highlight = window.document.createElement("div"),
elementStyling = window.getComputedStyle(element),
transitionDuration = parseFloat(elementStyling.getPropertyValue('transition-duration')),
animationDuration = parseFloat(elementStyling.getPropertyValue('animation-duration'));
if (transitionDuration) {
animateHighlight(transitionDuration);
}
if (animationDuration) {
animateHighlight(animationDuration);
}
// Don't highlight elements with 0 width & height
if (elementBounds.width === 0 && elementBounds.height === 0) {
return;
}
var realElBorder = {
right: elementStyling.getPropertyValue('border-right-width'),
left: elementStyling.getPropertyValue('border-left-width'),
top: elementStyling.getPropertyValue('border-top-width'),
bottom: elementStyling.getPropertyValue('border-bottom-width')
};
var borderBox = elementStyling.boxSizing === 'border-box';
var innerWidth = parseFloat(elementStyling.width),
innerHeight = parseFloat(elementStyling.height),
outerHeight = innerHeight,
outerWidth = innerWidth;
if (!borderBox) {
innerWidth += parseFloat(elementStyling.paddingLeft) + parseFloat(elementStyling.paddingRight);
innerHeight += parseFloat(elementStyling.paddingTop) + parseFloat(elementStyling.paddingBottom);
outerWidth = innerWidth + parseFloat(realElBorder.right) +
parseFloat(realElBorder.left),
outerHeight = innerHeight + parseFloat(realElBorder.bottom) + parseFloat(realElBorder.top);
}
var visualisations = {
horizontal: "left, right",
vertical: "top, bottom"
};
var drawPaddingRect = function(side) {
var elStyling = {};
if (visualisations.horizontal.indexOf(side) >= 0) {
elStyling['width'] = elementStyling.getPropertyValue('padding-' + side);
elStyling['height'] = innerHeight + "px";
elStyling['top'] = 0;
if (borderBox) {
elStyling['height'] = innerHeight - parseFloat(realElBorder.top) - parseFloat(realElBorder.bottom) + "px";
}
} else {
elStyling['height'] = elementStyling.getPropertyValue('padding-' + side);
elStyling['width'] = innerWidth + "px";
elStyling['left'] = 0;
if (borderBox) {
elStyling['width'] = innerWidth - parseFloat(realElBorder.left) - parseFloat(realElBorder.right) + "px";
}
}
elStyling[side] = 0;
elStyling['position'] = 'absolute';
return elStyling;
};
var drawMarginRect = function(side) {
var elStyling = {};
var margin = [];
margin['right'] = parseFloat(elementStyling.getPropertyValue('margin-right'));
margin['top'] = parseFloat(elementStyling.getPropertyValue('margin-top'));
margin['bottom'] = parseFloat(elementStyling.getPropertyValue('margin-bottom'));
margin['left'] = parseFloat(elementStyling.getPropertyValue('margin-left'));
if(visualisations['horizontal'].indexOf(side) >= 0) {
elStyling['width'] = elementStyling.getPropertyValue('margin-' + side);
elStyling['height'] = outerHeight + margin['top'] + margin['bottom'] + "px";
elStyling['top'] = "-" + (margin['top'] + parseFloat(realElBorder.top)) + "px";
} else {
elStyling['height'] = elementStyling.getPropertyValue('margin-' + side);
elStyling['width'] = outerWidth + "px";
elStyling['left'] = "-" + realElBorder.left;
}
elStyling[side] = "-" + (margin[side] + parseFloat(realElBorder[side])) + "px";
elStyling['position'] = 'absolute';
return elStyling;
};
var setVisibility = function (el) {
if (
!config.remoteHighlight.showPaddingMargin ||
parseInt(el.height, 10) <= 0 ||
parseInt(el.width, 10) <= 0
) {
el.display = 'none';
} else {
el.display = 'block';
}
};
var mainBoxStyles = config.remoteHighlight.stylesToSet;
var paddingVisualisations = [
drawPaddingRect('top'),
drawPaddingRect('right'),
drawPaddingRect('bottom'),
drawPaddingRect('left')
];
var marginVisualisations = [
drawMarginRect('top'),
drawMarginRect('right'),
drawMarginRect('bottom'),
drawMarginRect('left')
];
var setupVisualisations = function (arr, config) {
var i;
for (i = 0; i < arr.length; i++) {
setVisibility(arr[i]);
// Applies to every visualisationElement (padding or margin div)
arr[i]["transform"] = "none";
var el = window.document.createElement("div"),
styles = Object.assign(
{},
config,
arr[i]
);
_setStyleValues(styles, el.style);
highlight.appendChild(el);
}
};
setupVisualisations(
marginVisualisations,
config.remoteHighlight.marginStyling
);
setupVisualisations(
paddingVisualisations,
config.remoteHighlight.paddingStyling
);
highlight.className = HIGHLIGHT_CLASSNAME;
var offset = _screenOffset(element);
var el = element,
offsetLeft = 0,
offsetTop = 0;
// Probably the easiest way to get elements position without including transform
do {
offsetLeft += el.offsetLeft;
offsetTop += el.offsetTop;
el = el.offsetParent;
} while(el);
var stylesToSet = {
"left": offsetLeft + "px",
"top": offsetTop + "px",
"width": innerWidth + "px",
"height": innerHeight + "px",
"z-index": 2000000,
"margin": 0,
"padding": 0,
"position": "absolute",
"pointer-events": "none",
"box-shadow": "0 0 1px #fff",
"box-sizing": elementStyling.getPropertyValue('box-sizing'),
"border-right": elementStyling.getPropertyValue('border-right'),
"border-left": elementStyling.getPropertyValue('border-left'),
"border-top": elementStyling.getPropertyValue('border-top'),
"border-bottom": elementStyling.getPropertyValue('border-bottom'),
"transform": elementStyling.getPropertyValue('transform'),
"transform-origin": elementStyling.getPropertyValue('transform-origin'),
"border-color": config.remoteHighlight.borderColor
};
var mergedStyles = Object.assign({}, stylesToSet, config.remoteHighlight.stylesToSet);
var animateStartValues = config.remoteHighlight.animateStartValue;
var animateEndValues = config.remoteHighlight.animateEndValue;
var transitionValues = {
"transition-property": "opacity, background-color, transform",
"transition-duration": "300ms, 2.3s"
};
function _setStyleValues(styleValues, obj) {
var prop;
for (prop in styleValues) {
obj.setProperty(prop, styleValues[prop]);
}
}
_setStyleValues(mergedStyles, highlight.style);
_setStyleValues(
doAnimation ? animateStartValues : animateEndValues,
highlight.style
);
if (doAnimation) {
_setStyleValues(transitionValues, highlight.style);
window.setTimeout(function () {
_setStyleValues(animateEndValues, highlight.style);
}, 20);
}
window.document.body.appendChild(highlight);
},
add: function (element, doAnimation) {
if (this._elementExists(element) || element === window.document) {
return;
}
if (this.trigger) {
_trigger(element, "highlight", 1);
}
if ((!window.event || window.event instanceof MessageEvent) && !isInViewport(element)) {
var top = getDocumentOffsetTop(element);
if (top) {
top -= (window.innerHeight / 2);
window.scrollTo(0, top);
}
}
this.elements.push(element);
this._makeHighlightDiv(element, doAnimation);
},
clear: function () {
var i, highlights = window.document.querySelectorAll("." + HIGHLIGHT_CLASSNAME),
body = window.document.body;
for (i = 0; i < highlights.length; i++) {
body.removeChild(highlights[i]);
}
if (this.trigger) {
for (i = 0; i < this.elements.length; i++) {
_trigger(this.elements[i], "highlight", 0);
}
}
this.elements = [];
},
redraw: function () {
var i, highlighted;
// When redrawing a selector-based highlight, run a new selector
// query to ensure we have the latest set of elements to highlight.
if (this.selector) {
highlighted = window.document.querySelectorAll(this.selector);
} else {
highlighted = this.elements.slice(0);
}
this.clear();
for (i = 0; i < highlighted.length; i++) {
this.add(highlighted[i], false);
}
}
};
var _currentEditor;
function _toggleEditor(element) {
_currentEditor = new Editor(element);
}
var _currentMenu;
function _toggleMenu(element) {
if (_currentMenu) {
_currentMenu.remove();
}
_currentMenu = new Menu(element);
}
var _localHighlight;
var _remoteHighlight;
var _setup = false;
Constructor
function DOMEditHandler(htmlDocument) {
this.htmlDocument = htmlDocument;
this.rememberedNodes = null;
this.entityParseParent = htmlDocument.createElement("div");
}
RemoteFunctions define the functions to be executed in the browser. This modules should define a single function that returns an object of all exported functions.
function RemoteFunctions(config, remoteWSPort) {
"use strict";
var experimental;
if (!config) {
experimental = false;
} else {
experimental = config.experimental;
}
var lastKeepAliveTime = Date.now();
var req, timeout;
var animateHighlight = function (time) {
if(req) {
window.cancelAnimationFrame(req);
window.clearTimeout(timeout);
}
req = window.requestAnimationFrame(redrawHighlights);
timeout = setTimeout(function () {
window.cancelAnimationFrame(req);
req = null;
}, time * 1000);
};
function _domElementToJSON(elem) {
var json = { tag: elem.tagName.toLowerCase(), attributes: {}, children: [] },
i,
len,
node,
value;
len = elem.attributes.length;
for (i = 0; i < len; i++) {
node = elem.attributes.item(i);
value = (node.name === "data-brackets-id") ? parseInt(node.value, 10) : node.value;
json.attributes[node.name] = value;
}
len = elem.childNodes.length;
for (i = 0; i < len; i++) {
node = elem.childNodes.item(i);
// ignores comment nodes and visuals generated by live preview
if (node.nodeType === Node.ELEMENT_NODE && node.className !== HIGHLIGHT_CLASSNAME) {
json.children.push(_domElementToJSON(node));
} else if (node.nodeType === Node.TEXT_NODE) {
json.children.push({ content: node.nodeValue });
}
}
return json;
}
function getSimpleDOM() {
return JSON.stringify(_domElementToJSON(window.document.documentElement));
}
function updateConfig(newConfig) {
config = JSON.parse(newConfig);
return JSON.stringify(config);
}
// init
_editHandler = new DOMEditHandler(window.document);
if (experimental) {
window.document.addEventListener("keydown", onKeyDown);
}
var _ws = null;
function onDocumentClick(event) {
var element = event.target,
currentDataId,
newDataId;
if (_ws && element && element.hasAttribute('data-brackets-id')) {
_ws.send(JSON.stringify({
type: "message",
message: element.getAttribute('data-brackets-id')
}));
}
}
function createWebSocket() {
_ws = new WebSocket("ws://localhost:" + remoteWSPort);
_ws.onopen = function () {
window.document.addEventListener("click", onDocumentClick);
};
_ws.onmessage = function (evt) {
};
_ws.onclose = function () {
// websocket is closed
window.document.removeEventListener("click", onDocumentClick);
};
}
if (remoteWSPort) {
createWebSocket();
}
return {
"DOMEditHandler" : DOMEditHandler,
"keepAlive" : keepAlive,
"showGoto" : showGoto,
"hideHighlight" : hideHighlight,
"highlight" : highlight,
"highlightRule" : highlightRule,
"redrawHighlights" : redrawHighlights,
"applyDOMEdits" : applyDOMEdits,
"getSimpleDOM" : getSimpleDOM,
"updateConfig" : updateConfig
};
}
function _isRawTextNode(node) {
return (node.nodeType === Node.ELEMENT_NODE && /script|style|noscript|noframes|noembed|iframe|xmp/i.test(node.tagName));
}
Event Handlers **
function onMouseOver(event) {
if (_validEvent(event)) {
_localHighlight.add(event.target, true);
}
}
function onMouseOut(event) {
if (_validEvent(event)) {
_localHighlight.clear();
}
}
function onMouseMove(event) {
onMouseOver(event);
window.document.removeEventListener("mousemove", onMouseMove);
}
function onClick(event) {
if (_validEvent(event)) {
event.preventDefault();
event.stopPropagation();
if (event.altKey) {
_toggleEditor(event.target);
} else {
_toggleMenu(event.target);
}
}
}
function onKeyUp(event) {
if (_setup && !_validEvent(event)) {
window.document.removeEventListener("keyup", onKeyUp);
window.document.removeEventListener("mouseover", onMouseOver);
window.document.removeEventListener("mouseout", onMouseOut);
window.document.removeEventListener("mousemove", onMouseMove);
window.document.removeEventListener("click", onClick);
_localHighlight.clear();
_localHighlight = undefined;
_setup = false;
}
}
function onKeyDown(event) {
if (!_setup && _validEvent(event)) {
window.document.addEventListener("keyup", onKeyUp);
window.document.addEventListener("mouseover", onMouseOver);
window.document.addEventListener("mouseout", onMouseOut);
window.document.addEventListener("mousemove", onMouseMove);
window.document.addEventListener("click", onClick);
_localHighlight = new Highlight("#ecc", true);
_setup = true;
}
}