diff --git a/src/LiveDevelopment/Agents/RemoteAgent.js b/src/LiveDevelopment/Agents/RemoteAgent.js index 0f373c343bd..47e309c600e 100644 --- a/src/LiveDevelopment/Agents/RemoteAgent.js +++ b/src/LiveDevelopment/Agents/RemoteAgent.js @@ -39,10 +39,14 @@ define(function RemoteAgent(require, exports, module) { var LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), Inspector = require("LiveDevelopment/Inspector/Inspector"), - RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"); + RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"), + jQueryRemoteSrc = require("text!thirdparty/jQuery-2.0.1.min.js"); + + var $REMOTE = "window._LDjQuery"; var _load; // deferred load var _objectId; // the object id of the remote object + var _jQueryRemoteObjectId; var _intervalId; // interval used to send keepAlive events // WebInspector Event: DOM.attributeModified @@ -54,40 +58,61 @@ define(function RemoteAgent(require, exports, module) { } } - /** 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); + function _call(objectId, method, varargs) { + console.assert(objectId, "Attempted to call remote method without objectId set."); + var args = Array.prototype.slice.call(arguments, 2), + callback, + deferred = new $.Deferred(); // 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(); + args = args.map(function (arg) { + if (arg && arg.nodeId) { + return arg.resolve(); } - } + + return arg; + }); + $.when.apply(undefined, args).done(function onResolvedAllNodes() { - var i, arg, params = []; - for (i in arguments) { - arg = args[i]; + var params = []; + + args.forEach(function (arg) { if (arg.objectId) { params.push({objectId: arg.objectId}); } else { params.push({value: arg}); } - } - Inspector.Runtime.callFunctionOn(_objectId, "_LD." + method, params, undefined, callback); + }); + + Inspector.Runtime.callFunctionOn(objectId, method, params, undefined, callback) + .then(deferred.resolve, deferred.reject); }); + + return deferred.promise(); + } + + /** 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) { + var argsArray = [_objectId, "_LD." + method]; + + if (arguments.length > 1) { + argsArray = argsArray.concat(Array.prototype.slice.call(arguments, 1)); + } + + return _call.apply(null, argsArray); + } + + function jQueryRemote(method, varargs) { + return _call(_jQueryRemoteObjectId, "_LDjQuery." + method, varargs); } function _stopKeepAliveInterval() { @@ -116,7 +141,9 @@ define(function RemoteAgent(require, exports, module) { // WebInspector Event: Page.loadEventFired function _onLoadEventFired(event, res) { // res = {timestamp} - var command = "window._LD=" + RemoteFunctions + "(" + LiveDevelopment.config.experimental + ")"; + + // inject RemoteFunctions + var command = "window._LD=" + RemoteFunctions + "(" + LiveDevelopment.config.experimental + ");"; Inspector.Runtime.evaluate(command, function onEvaluate(response) { if (response.error || response.wasThrown) { @@ -128,6 +155,17 @@ define(function RemoteAgent(require, exports, module) { _startKeepAliveInterval(); } }); + + // inject jQuery + command = jQueryRemoteSrc + "window._LDjQuery=jQuery.noConflict(true);"; + + Inspector.Runtime.evaluate(command, function onEvaluate(response) { + if (response.error || response.wasThrown) { + _load.reject(null, response.error); + } else { + _jQueryRemoteObjectId = response.result.objectId; + } + }); } /** Initialize the agent */ @@ -147,8 +185,44 @@ define(function RemoteAgent(require, exports, module) { _stopKeepAliveInterval(); } + // Prototype DOM manipulation + var REMOTE_ELEMENT_METHODS = [ + "attr", "removeAttr", + "before", "after", "append", "prepend", + "text", + "detach", "remove", + "html", + "replaceWith" + ]; + + function RemoteElement(dataBracketsId) { + var self = this; + + this._queryBracketsId = "var $result = " + $REMOTE + '("[data-brackets-id=\\"' + dataBracketsId + '\\"]");'; + this._dataBracketsId = dataBracketsId; + + REMOTE_ELEMENT_METHODS.forEach(function (methodName) { + self[methodName] = self._eval.bind(self, methodName); + }); + } + + RemoteElement.prototype._eval = function (method, varargs) { + // Convert method arguments to JSON string to escape string args + var argsArray = JSON.stringify(Array.prototype.slice.call(arguments, 1)).replace(/\\/g, "\\\\").replace(/\"/g, "\\\""), + argsAssign = "var args = JSON.parse(\"" + argsArray + "\");", + fnApply = "$result." + method + ".apply($result, args)"; + + return Inspector.Runtime.evaluate(argsAssign + this._queryBracketsId + fnApply); + }; + + function remoteElement(dataBracketsId) { + return new RemoteElement(dataBracketsId); + } + // Export public functions exports.call = call; + exports.jQuery = jQueryRemote; exports.load = load; exports.unload = unload; + exports.remoteElement = remoteElement; }); diff --git a/src/LiveDevelopment/Documents/HTMLDocument.js b/src/LiveDevelopment/Documents/HTMLDocument.js index f10b53e5fa1..5ac6bcc59bf 100644 --- a/src/LiveDevelopment/Documents/HTMLDocument.js +++ b/src/LiveDevelopment/Documents/HTMLDocument.js @@ -49,7 +49,8 @@ define(function HTMLDocumentModule(require, exports, module) { HighlightAgent = require("LiveDevelopment/Agents/HighlightAgent"), HTMLInstrumentation = require("language/HTMLInstrumentation"), Inspector = require("LiveDevelopment/Inspector/Inspector"), - LiveDevelopment = require("LiveDevelopment/LiveDevelopment"); + LiveDevelopment = require("LiveDevelopment/LiveDevelopment"), + RemoteAgent = require("LiveDevelopment/Agents/RemoteAgent"); /** Constructor * @@ -74,10 +75,10 @@ define(function HTMLDocumentModule(require, exports, module) { // Used by highlight agent to highlight editor text as selected in browser this.onHighlight = this.onHighlight.bind(this); $(HighlightAgent).on("highlight", this.onHighlight); - - this.onChange = this.onChange.bind(this); - $(this.editor).on("change", this.onChange); } + + this.onChange = this.onChange.bind(this); + $(this.editor).on("change", this.onChange); }; /** @@ -149,17 +150,34 @@ define(function HTMLDocumentModule(require, exports, module) { /** Triggered on change by the editor */ HTMLDocument.prototype.onChange = function onChange(event, editor, change) { - if (!this.editor) { - return; - } - 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; + var marker = HTMLInstrumentation._getMarkerAtDocumentPos( + this.editor, + editor.getCursorPos() + ); + + // HACK, replace the whole tag + if (marker && marker.tagID) { + var range = marker.find(), + text = marker.doc.getRange(range.from, range.to); + + // HACK maintain ID + text = text.replace(">", " data-brackets-id='" + marker.tagID + "'>"); + + // FIXME incorrectly replaces body elements with content only, missing body element + RemoteAgent.remoteElement(marker.tagID).replaceWith(text); } + + // if (!this.editor) { + // return; + // } + // 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 */ diff --git a/src/language/HTMLInstrumentation.js b/src/language/HTMLInstrumentation.js index 2615ee7ef1f..7e1f4201c97 100644 --- a/src/language/HTMLInstrumentation.js +++ b/src/language/HTMLInstrumentation.js @@ -269,19 +269,8 @@ define(function (require, exports, module) { mark.tagID = tag.tagID; }); } - - /** - * Get the instrumented tagID at the specified position. Returns -1 if - * there are no instrumented tags at the location. - * The _markText() function must be called before calling this function. - * - * NOTE: This function is "private" for now (has a leading underscore), since - * the API is likely to change in the future. - * - * @param {Editor} editor The editor to scan. - * @return {number} tagID at the specified position, or -1 if there is no tag - */ - function _getTagIDAtDocumentPos(editor, pos) { + + function _getMarkerAtDocumentPos(editor, pos) { var i, cm = editor._codeMirror, marks = cm.findMarksAt(pos), @@ -307,11 +296,24 @@ define(function (require, exports, module) { } } - if (match) { - return match.tagID; - } - - return -1; + return match; + } + + /** + * Get the instrumented tagID at the specified position. Returns -1 if + * there are no instrumented tags at the location. + * The _markText() function must be called before calling this function. + * + * NOTE: This function is "private" for now (has a leading underscore), since + * the API is likely to change in the future. + * + * @param {Editor} editor The editor to scan. + * @return {number} tagID at the specified position, or -1 if there is no tag + */ + function _getTagIDAtDocumentPos(editor, pos) { + var match = _getMarkerAtDocumentPos(editor, pos); + + return (match) ? match.tagID : -1; } $(DocumentManager).on("beforeDocumentDelete", _removeDocFromCache); @@ -319,5 +321,6 @@ define(function (require, exports, module) { exports.scanDocument = scanDocument; exports.generateInstrumentedHTML = generateInstrumentedHTML; exports._markText = _markText; + exports._getMarkerAtDocumentPos = _getMarkerAtDocumentPos; exports._getTagIDAtDocumentPos = _getTagIDAtDocumentPos; });