Skip to content

Commit e93cffb

Browse files
committed
feat: add cut copy paste buttons in options box
1 parent 6b53b70 commit e93cffb

File tree

2 files changed

+206
-1
lines changed

2 files changed

+206
-1
lines changed

src/LiveDevelopment/BrowserScripts/RemoteFunctions.js

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,70 @@ function RemoteFunctions(config = {}) {
358358
}
359359
}
360360

361+
/**
362+
* this is for cut button, when user clicks on cut button we copy the element's source code
363+
* into the clipboard and remove it from the src code. read `_cutElementToClipboard` in `LivePreviewEdit.js`
364+
* @param {Event} event
365+
* @param {DOMElement} element - the element we need to cut
366+
*/
367+
function _handleCutOptionClick(event, element) {
368+
if (isElementEditable(element)) {
369+
const tagId = element.getAttribute("data-brackets-id");
370+
371+
window._Brackets_MessageBroker.send({
372+
livePreviewEditEnabled: true,
373+
element: element,
374+
event: event,
375+
tagId: Number(tagId),
376+
cut: true
377+
});
378+
} else {
379+
console.error("The TagID might be unavailable or the element tag is directly body or html");
380+
}
381+
}
382+
383+
/**
384+
* this is for copy button, similar to cut just we don't remove the elements source code
385+
* @param {Event} event
386+
* @param {DOMElement} element
387+
*/
388+
function _handleCopyOptionClick(event, element) {
389+
if (isElementEditable(element)) {
390+
const tagId = element.getAttribute("data-brackets-id");
391+
392+
window._Brackets_MessageBroker.send({
393+
livePreviewEditEnabled: true,
394+
element: element,
395+
event: event,
396+
tagId: Number(tagId),
397+
copy: true
398+
});
399+
} else {
400+
console.error("The TagID might be unavailable or the element tag is directly body or html");
401+
}
402+
}
403+
404+
/**
405+
* this is for paste button, this inserts the saved content from clipboard just above this element
406+
* @param {Event} event
407+
* @param {DOMElement} targetElement
408+
*/
409+
function _handlePasteOptionClick(event, targetElement) {
410+
if (isElementEditable(targetElement)) {
411+
const targetTagId = targetElement.getAttribute("data-brackets-id");
412+
413+
window._Brackets_MessageBroker.send({
414+
livePreviewEditEnabled: true,
415+
element: targetElement,
416+
event: event,
417+
tagId: Number(targetTagId),
418+
paste: true
419+
});
420+
} else {
421+
console.error("The TagID might be unavailable or the element tag is directly body or html");
422+
}
423+
}
424+
361425
/**
362426
* this is for select-parent button
363427
* When user clicks on this option for a particular element, we get its parent element and trigger a click on it
@@ -400,6 +464,12 @@ function RemoteFunctions(config = {}) {
400464
_handleDuplicateOptionClick(e, element);
401465
} else if (action === "delete") {
402466
_handleDeleteOptionClick(e, element);
467+
} else if (action === "cut") {
468+
_handleCutOptionClick(e, element);
469+
} else if (action === "copy") {
470+
_handleCopyOptionClick(e, element);
471+
} else if (action === "paste") {
472+
_handlePasteOptionClick(e, element);
403473
} else if (action === "ai") {
404474
_handleAIOptionClick(e, element);
405475
} else if (action === "image-gallery") {
@@ -1360,6 +1430,30 @@ function RemoteFunctions(config = {}) {
13601430
</svg>
13611431
`,
13621432

1433+
cut: `
1434+
<svg viewBox="0 0 24 24" fill="currentColor">
1435+
<circle cx="6" cy="6" r="3"/>
1436+
<circle cx="6" cy="18" r="3"/>
1437+
<line x1="20" y1="4" x2="8.12" y2="15.88" stroke="currentColor" stroke-width="2"/>
1438+
<line x1="14.47" y1="14.48" x2="20" y2="20" stroke="currentColor" stroke-width="2"/>
1439+
<line x1="8.12" y1="8.12" x2="12" y2="12" stroke="currentColor" stroke-width="2"/>
1440+
</svg>
1441+
`,
1442+
1443+
copy: `
1444+
<svg viewBox="0 0 24 24" fill="currentColor">
1445+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2"/>
1446+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
1447+
</svg>
1448+
`,
1449+
1450+
paste: `
1451+
<svg viewBox="0 0 24 24" fill="currentColor">
1452+
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
1453+
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
1454+
</svg>
1455+
`,
1456+
13631457
imageGallery: `
13641458
<svg viewBox="0 0 24 24" fill="currentColor">
13651459
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
@@ -1512,6 +1606,15 @@ function RemoteFunctions(config = {}) {
15121606
<span data-action="delete" title="${config.strings.delete}">
15131607
${ICONS.trash}
15141608
</span>
1609+
<span data-action="cut" title='Cut'>
1610+
${ICONS.cut}
1611+
</span>
1612+
<span data-action="copy" title='Copy'>
1613+
${ICONS.copy}
1614+
</span>
1615+
<span data-action="paste" title='Paste'>
1616+
${ICONS.paste}
1617+
</span>
15151618
</div>`;
15161619

15171620
let styles = `
@@ -1614,7 +1717,7 @@ function RemoteFunctions(config = {}) {
16141717
span.addEventListener('click', (event) => {
16151718
event.stopPropagation();
16161719
event.preventDefault();
1617-
// data-action is to differentiate between the buttons (duplicate, delete or select-parent)
1720+
// data-action is to differentiate between the buttons (duplicate, delete, select-parent etc)
16181721
const action = event.currentTarget.getAttribute('data-action');
16191722
handleOptionClick(event, action, this.element);
16201723
if (action !== 'duplicate') {

src/LiveDevelopment/LivePreviewEdit.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,102 @@ define(function (require, exports, module) {
315315
});
316316
}
317317

318+
/**
319+
* this saves the element to clipboard and deletes its source code
320+
* @param {Number} tagId
321+
*/
322+
function _cutElementToClipboard(tagId) {
323+
const editor = _getEditorAndValidate(tagId);
324+
if (!editor) {
325+
return;
326+
}
327+
328+
const range = _getElementRange(editor, tagId);
329+
if (!range) {
330+
return;
331+
}
332+
333+
const { startPos, endPos } = range;
334+
const text = editor.getTextBetween(startPos, endPos);
335+
336+
Phoenix.app.copyToClipboard(text);
337+
338+
// delete the elements source code
339+
editor.document.batchOperation(function () {
340+
editor.replaceRange("", startPos, endPos);
341+
342+
// clean up any empty line
343+
if(startPos.line !== 0 && !(editor.getLine(startPos.line).trim())) {
344+
const prevLineText = editor.getLine(startPos.line - 1);
345+
const chPrevLine = prevLineText ? prevLineText.length : 0;
346+
editor.replaceRange("", {line: startPos.line - 1, ch: chPrevLine}, startPos);
347+
}
348+
});
349+
}
350+
351+
function _copyElementToClipboard(tagId) {
352+
const editor = _getEditorAndValidate(tagId);
353+
if (!editor) {
354+
return;
355+
}
356+
357+
const range = _getElementRange(editor, tagId);
358+
if (!range) {
359+
return;
360+
}
361+
362+
const { startPos, endPos } = range;
363+
const text = editor.getTextBetween(startPos, endPos);
364+
365+
Phoenix.app.copyToClipboard(text);
366+
}
367+
368+
/**
369+
* this function is to paste the clipboard content above the target element
370+
* @param {Number} tagId
371+
*/
372+
function _pasteElementFromClipboard(tagId) {
373+
const editor = _getEditorAndValidate(tagId);
374+
if (!editor) {
375+
return;
376+
}
377+
const range = _getElementRange(editor, tagId);
378+
if (!range) {
379+
return;
380+
}
381+
382+
const { startPos } = range;
383+
384+
Phoenix.app.clipboardReadText().then(text => {
385+
if (!text) {
386+
return;
387+
}
388+
389+
// get the indentation at the target element's line
390+
const indent = editor.getTextBetween({ line: startPos.line, ch: 0 }, startPos);
391+
392+
// for proper indentation
393+
const lines = text.split('\n');
394+
const indentedLines = lines.map((line, index) => {
395+
if (index === 0) {
396+
return indent.trim() === "" ? indent + line : line;
397+
}
398+
return line ? indent + line : line;
399+
});
400+
const indentedContent = indentedLines.join('\n');
401+
402+
editor.document.batchOperation(function () {
403+
if (indent.trim() === "") {
404+
editor.replaceRange(indentedContent + "\n", startPos);
405+
} else {
406+
editor.replaceRange("\n" + indentedContent, { line: startPos.line, ch: 0 });
407+
}
408+
});
409+
}).catch(err => {
410+
console.error("Failed to read from clipboard:", err);
411+
});
412+
}
413+
318414
/**
319415
* This function is responsible to delete an element from the source code
320416
* @param {Number} tagId - the data-brackets-id of the DOM element
@@ -1387,6 +1483,12 @@ define(function (require, exports, module) {
13871483
_deleteElementInSourceByTagId(message.tagId);
13881484
} else if (message.duplicate) {
13891485
_duplicateElementInSourceByTagId(message.tagId);
1486+
} else if (message.cut) {
1487+
_cutElementToClipboard(message.tagId);
1488+
} else if (message.copy) {
1489+
_copyElementToClipboard(message.tagId);
1490+
} else if (message.paste) {
1491+
_pasteElementFromClipboard(message.tagId);
13901492
} else if (message.livePreviewTextEdit) {
13911493
_editTextInSource(message);
13921494
} else if (message.AISend) {

0 commit comments

Comments
 (0)