Skip to content

Commit 59dcddb

Browse files
committed
feat: keyboard shortcuts for cut/copy/paste working in live preview edit mode
1 parent 68b79aa commit 59dcddb

File tree

3 files changed

+195
-28
lines changed

3 files changed

+195
-28
lines changed

src/editor/EditorHelper/ChangeHelper.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525

2626
define(function (require, exports, module) {
2727

28+
let _cutInterceptor = null;
29+
let _copyInterceptor = null;
30+
let _pasteInterceptor = null;
31+
2832
const CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"),
2933
Menus = require("command/Menus");
3034

@@ -242,6 +246,29 @@ define(function (require, exports, module) {
242246
elt.style.textIndent = "-" + off + "px";
243247
elt.style.paddingLeft = off + "px";
244248
});
249+
self._codeMirror.on("cut", function(cm, e) {
250+
// Let interceptor decide what to do with the event (including preventDefault)
251+
if (_cutInterceptor) {
252+
return _cutInterceptor(self, cm, e);
253+
}
254+
// Otherwise allow normal cut behavior
255+
});
256+
257+
self._codeMirror.on("copy", function(cm, e) {
258+
// Let interceptor decide what to do with the event (including preventDefault)
259+
if (_copyInterceptor) {
260+
return _copyInterceptor(self, cm, e);
261+
}
262+
// Otherwise allow normal copy behavior
263+
});
264+
265+
self._codeMirror.on("paste", function(cm, e) {
266+
// Let interceptor decide what to do with the event (including preventDefault)
267+
if (_pasteInterceptor) {
268+
return _pasteInterceptor(self, cm, e);
269+
}
270+
// Otherwise allow normal paste behavior
271+
});
245272
}
246273

247274
/**
@@ -282,5 +309,35 @@ define(function (require, exports, module) {
282309
Editor.prototype._dontDismissPopupOnScroll = _dontDismissPopupOnScroll;
283310
}
284311

312+
/**
313+
* Sets the cut interceptor function
314+
* @param {Function} interceptor - Function(editor, cm, event) that returns true to
315+
preventDefault
316+
*/
317+
function setCutInterceptor(interceptor) {
318+
_cutInterceptor = interceptor;
319+
}
320+
321+
/**
322+
* Sets the copy interceptor function
323+
* @param {Function} interceptor - Function(editor, cm, event) that returns true to
324+
preventDefault
325+
*/
326+
function setCopyInterceptor(interceptor) {
327+
_copyInterceptor = interceptor;
328+
}
329+
330+
/**
331+
* Sets the paste interceptor function
332+
* @param {Function} interceptor - Function(editor, cm, event) that returns true to
333+
preventDefault
334+
*/
335+
function setPasteInterceptor(interceptor) {
336+
_pasteInterceptor = interceptor;
337+
}
338+
285339
exports.addHelpers =addHelpers;
340+
exports.setCutInterceptor = setCutInterceptor;
341+
exports.setCopyInterceptor = setCopyInterceptor;
342+
exports.setPasteInterceptor = setPasteInterceptor;
286343
});

src/extensionsIntegrated/phoenix-pro/LivePreviewEdit.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ define(function (require, exports, module) {
2929
const ImageFolderDialogTemplate = require("text!./html/image-folder-dialog.html");
3030
const LiveDevProtocol = require("LiveDevelopment/MultiBrowserImpl/protocol/LiveDevProtocol");
3131
const ProUtils = require("./pro-utils");
32+
const EditorManager = require("editor/EditorManager");
33+
const ChangeHelper = require("editor/EditorHelper/ChangeHelper");
34+
const AppInit = require("utils/AppInit");
35+
const Editor = require("editor/Editor").Editor;
3236

3337
// state manager key, to save the download location of the image
3438
const IMAGE_DOWNLOAD_FOLDER_KEY = "imageGallery.downloadFolder";
@@ -654,9 +658,15 @@ define(function (require, exports, module) {
654658
// get the indentation in the target line and check if there is any real indentation
655659
let indent = editor.getTextBetween({ line: startPos.line, ch: 0 }, startPos);
656660
indent = indent.trim() === '' ? `\n${indent}` : '';
657-
658-
editor.replaceRange(indent + text, endPos);
659-
editor.setCursorPos(endPos.line + 1, indent.length);
661+
const finalReplacementText = indent + text;
662+
663+
editor.replaceRange(finalReplacementText, endPos);
664+
editor.setCursorPos(endPos.line + 1, indent.length - 1);
665+
// since this is called edither when user clicks on live preview paste button, or key board shortcut,
666+
// when live preview is in context, we set it to true. If we dont, the a cursor position change will happen
667+
// and we will exit live preview context. So when a user repeatedly presses ctrl+v, we will exit the context
668+
// if we dont set it to true here.
669+
_isInLivePreviewSelectionContext = true;
660670
}).catch(err => {
661671
console.error("Failed to read from clipboard:", err);
662672
});
@@ -1878,4 +1888,70 @@ define(function (require, exports, module) {
18781888
handleLivePreviewEditOperation(msg);
18791889
}
18801890
});
1891+
1892+
let _isInLivePreviewSelectionContext = false;
1893+
// Cut interceptor
1894+
ChangeHelper.setCutInterceptor(function(editor, cm, event) {
1895+
if (_isInLivePreviewSelectionContext) {
1896+
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
1897+
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
1898+
event.preventDefault();
1899+
currLiveDoc.protocol.evaluate(`_LD.handleCutElement()`);
1900+
}
1901+
}
1902+
});
1903+
1904+
// Copy interceptor
1905+
ChangeHelper.setCopyInterceptor(function(editor, cm, event) {
1906+
if (_isInLivePreviewSelectionContext) {
1907+
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
1908+
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
1909+
event.preventDefault();
1910+
currLiveDoc.protocol.evaluate(`_LD.handleCopyElement()`);
1911+
}
1912+
}
1913+
});
1914+
1915+
// Paste interceptor
1916+
ChangeHelper.setPasteInterceptor(function(editor, cm, event) {
1917+
if (_isInLivePreviewSelectionContext) {
1918+
const currLiveDoc = LiveDevMultiBrowser.getCurrentLiveDoc();
1919+
if (currLiveDoc && currLiveDoc.protocol && currLiveDoc.protocol.evaluate) {
1920+
event.preventDefault();
1921+
currLiveDoc.protocol.evaluate(`_LD.handlePasteElement()`);
1922+
}
1923+
}
1924+
});
1925+
1926+
function _cursorActivity() {
1927+
if(_isInLivePreviewSelectionContext) {
1928+
window.logger.livePreview.log("unlocked cut/copy/paste from live preview");
1929+
}
1930+
_isInLivePreviewSelectionContext = false;
1931+
}
1932+
1933+
const LIVE_EDIT_SCOPE = ".LiveEditMain";
1934+
AppInit.appReady(function () {
1935+
EditorManager.on(EditorManager.EVENT_ACTIVE_EDITOR_CHANGED + LIVE_EDIT_SCOPE, (_event, newEditor, oldEditor)=>{
1936+
if(_isInLivePreviewSelectionContext) {
1937+
window.logger.livePreview.log("unlocked cut/copy/paste from live preview");
1938+
}
1939+
_isInLivePreviewSelectionContext = false;
1940+
if(oldEditor){
1941+
oldEditor.off(Editor.EVENT_CURSOR_ACTIVITY + LIVE_EDIT_SCOPE);
1942+
}
1943+
if(newEditor){
1944+
newEditor.off(Editor.EVENT_CURSOR_ACTIVITY + LIVE_EDIT_SCOPE);
1945+
newEditor.on(Editor.EVENT_CURSOR_ACTIVITY + LIVE_EDIT_SCOPE, _cursorActivity);
1946+
}
1947+
});
1948+
const activeEditor = EditorManager.getActiveEditor();
1949+
if(activeEditor) {
1950+
activeEditor.on(Editor.EVENT_CURSOR_ACTIVITY + LIVE_EDIT_SCOPE, _cursorActivity);
1951+
}
1952+
LiveDevelopment.on(LiveDevelopment.EVENT_LIVE_PREVIEW_CLICKED, ()=>{
1953+
window.logger.livePreview.log("cut/copy/paste locked to live preview");
1954+
_isInLivePreviewSelectionContext = true;
1955+
});
1956+
});
18811957
});

src/extensionsIntegrated/phoenix-pro/browser-context/generic-tools.js

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
* SPDX-License-Identifier: LicenseRef-Proprietary
44
*/
55

6-
/*global GLOBALS, LivePreviewView, cssStyles, strings, icons, proConstants*/
6+
/*global GLOBALS, LivePreviewView, customReturns, strings, icons, proConstants*/
7+
8+
let selectedElement;
79

810
// duplicate option
911
/**
@@ -163,27 +165,30 @@ function _renderCutDropdown() {
163165
};
164166
}
165167

166-
/**
167-
* this is for cut button, when user clicks on cut button we copy the element's source code
168-
* into the clipboard and remove it from the src code. read `_cutElementToClipboard` in `LivePreviewEdit.js`
169-
* @param {Event} event
170-
* @param {DOMElement} element - the element we need to cut
171-
* @param {DOMElement} _dropdown
172-
*/
173-
function _handleCutOptionClick(event, element, _dropdown) {
168+
function _handleCut(element) {
174169
if (LivePreviewView.isElementEditable(element)) {
175170
const tagId = element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
176171

177172
window._Brackets_MessageBroker.send({
178173
livePreviewEditEnabled: true,
179174
element: element,
180-
event: event,
181175
tagId: Number(tagId),
182176
cut: true
183177
});
184178
} else {
185179
console.error("The TagID might be unavailable or the element tag is directly body or html");
186180
}
181+
}
182+
183+
/**
184+
* this is for cut button, when user clicks on cut button we copy the element's source code
185+
* into the clipboard and remove it from the src code. read `_cutElementToClipboard` in `LivePreviewEdit.js`
186+
* @param {Event} _event
187+
* @param {DOMElement} element - the element we need to cut
188+
* @param {DOMElement} _dropdown
189+
*/
190+
function _handleCutOptionClick(_event, element, _dropdown) {
191+
_handleCut(element);
187192
return true;
188193
}
189194

@@ -203,20 +208,13 @@ function _renderCopyDropdown() {
203208
};
204209
}
205210

206-
/**
207-
* this is for copy button, similar to cut just we don't remove the elements source code
208-
* @param {Event} event
209-
* @param {DOMElement} element
210-
* @param {DOMElement} _dropdown
211-
*/
212-
function _handleCopyOptionClick(event, element, _dropdown) {
211+
function _handleCopy(element) {
213212
if (LivePreviewView.isElementEditable(element)) {
214213
const tagId = element.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
215214

216215
window._Brackets_MessageBroker.send({
217216
livePreviewEditEnabled: true,
218217
element: element,
219-
event: event,
220218
tagId: Number(tagId),
221219
copy: true
222220
});
@@ -226,6 +224,14 @@ function _handleCopyOptionClick(event, element, _dropdown) {
226224
return true;
227225
}
228226

227+
/**
228+
* this is for copy button, similar to cut just we don't remove the elements source code
229+
*/
230+
function _handleCopyOptionClick(_event, element, _dropdown) {
231+
_handleCopy(element);
232+
return true;
233+
}
234+
229235
LivePreviewView.registerNodeMoreOptionsHandler("copy", {
230236
renderDropdownItems: _renderCopyDropdown,
231237
handleDropdownClick: _handleCopyOptionClick
@@ -241,25 +247,28 @@ function _renderPasteDropdown() {
241247
};
242248
}
243249

244-
/**
245-
* this is for paste button, this inserts the saved content from clipboard just above this element
246-
* @param {Event} event
247-
* @param {DOMElement} targetElement
248-
*/
249-
function _handlePasteOptionClick(event, targetElement) {
250+
function _handlePaste(targetElement) {
250251
if (LivePreviewView.isElementEditable(targetElement)) {
251252
const targetTagId = targetElement.getAttribute(GLOBALS.DATA_BRACKETS_ID_ATTR);
252253

253254
window._Brackets_MessageBroker.send({
254255
livePreviewEditEnabled: true,
255256
element: targetElement,
256-
event: event,
257257
tagId: Number(targetTagId),
258258
paste: true
259259
});
260260
} else {
261261
console.error("The TagID might be unavailable or the element tag is directly body or html");
262262
}
263+
return true;
264+
}
265+
266+
/**
267+
* this is for paste button, this inserts the saved content from clipboard just above this element
268+
*/
269+
function _handlePasteOptionClick(_event, targetElement) {
270+
_handlePaste(targetElement);
271+
return true;
263272
}
264273

265274
LivePreviewView.registerNodeMoreOptionsHandler("paste", {
@@ -276,3 +285,28 @@ LivePreviewView.registerNodeMoreOptionsHandler("cutPasteSeparator", {
276285
};
277286
}
278287
});
288+
289+
LivePreviewView.registerNodeMoreOptionsHandler("elementSelectionObserver", {
290+
onElementSelected: (_selectedElement)=>{
291+
selectedElement = _selectedElement;
292+
},
293+
dismiss: ()=>{
294+
selectedElement = null;
295+
}
296+
});
297+
298+
customReturns.handleCutElement = ()=> {
299+
if(selectedElement){
300+
_handleCut(selectedElement);
301+
}
302+
};
303+
customReturns.handleCopyElement = ()=> {
304+
if(selectedElement){
305+
_handleCopy(selectedElement);
306+
}
307+
};
308+
customReturns.handlePasteElement = ()=> {
309+
if(selectedElement){
310+
_handlePaste(selectedElement);
311+
}
312+
};

0 commit comments

Comments
 (0)