mirror of
https://github.com/penpot/penpot.git
synced 2025-12-28 17:09:03 -05:00
Compare commits
18 Commits
develop
...
niwinz-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbe52e062 | ||
|
|
e600d23bed | ||
|
|
c3bf31e199 | ||
|
|
0788163744 | ||
|
|
835f0fae6a | ||
|
|
50368c5dd8 | ||
|
|
2bbc746919 | ||
|
|
12e88da901 | ||
|
|
2ba27314d0 | ||
|
|
80b86c81c1 | ||
|
|
ef12a28411 | ||
|
|
8aa523c6cc | ||
|
|
42e17481ae | ||
|
|
aab79bdfb3 | ||
|
|
0fbce5eb17 | ||
|
|
4ae5f8da94 | ||
|
|
5b3f1e41e4 | ||
|
|
f811198b07 |
@@ -114,7 +114,7 @@ jobs:
|
||||
# uses the same cache as this task so we prepopulate it
|
||||
command: |
|
||||
yarn install
|
||||
yarn run playwright install chromium
|
||||
yarn run playwright install chromium --with-deps
|
||||
|
||||
- run:
|
||||
name: "lint scss on frontend"
|
||||
@@ -249,8 +249,8 @@ jobs:
|
||||
name: "integration tests"
|
||||
working_directory: "./frontend"
|
||||
command: |
|
||||
yarn run playwright install chromium
|
||||
yarn run test:e2e -x --workers=4
|
||||
yarn run playwright install chromium --with-deps
|
||||
yarn run test:e2e -x --workers=4 --reporter=line
|
||||
|
||||
test-backend:
|
||||
docker:
|
||||
|
||||
59
.github/workflows/test-integration.yml
vendored
Normal file
59
.github/workflows/test-integration.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: "Integration Tests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- staging
|
||||
|
||||
jobs:
|
||||
test-integration:
|
||||
name: "test1"
|
||||
runs-on: ubuntu-24.04
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn install
|
||||
yarn run build:app:assets
|
||||
yarn run build:app
|
||||
yarn run build:app:libs
|
||||
|
||||
- name: Build WASM
|
||||
working-directory: "./render-wasm"
|
||||
run: |
|
||||
EMSDK_QUIET=1 . /opt/emsdk/emsdk_env.sh
|
||||
./build release
|
||||
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
yarn run playwright install chromium --with-deps
|
||||
yarn run test:e2e -x --workers=4 --reporter=line
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
@@ -90,6 +90,10 @@
|
||||
[{:fill-color clr/black
|
||||
:fill-opacity 1}])
|
||||
|
||||
(def default-paragraph-attrs
|
||||
{:text-align "left"
|
||||
:text-direction "ltr"})
|
||||
|
||||
(def default-text-attrs
|
||||
{:font-id "sourcesanspro"
|
||||
:font-family "sourcesanspro"
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"nodemon": "^3.1.10",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-limit": "^6.2.0",
|
||||
"playwright": "1.52.0",
|
||||
"playwright": "1.56.1",
|
||||
"postcss": "^8.5.4",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"prettier": "3.5.3",
|
||||
|
||||
@@ -11,15 +11,19 @@ import { defineConfig, devices } from "@playwright/test";
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "./playwright",
|
||||
outputDir: './test-results',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
forbidOnly: false,
|
||||
// forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests by default; can be overriden with --workers */
|
||||
workers: 1,
|
||||
/* Timeout for expects (longer in CI) */
|
||||
|
||||
timeout: 40000,
|
||||
expect: {
|
||||
timeout: process.env.CI ? 20000 : 5000,
|
||||
},
|
||||
@@ -45,6 +49,10 @@ export default defineConfig({
|
||||
name: "default",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
testDir: "./playwright/ui/specs",
|
||||
use: {
|
||||
video: 'retain-on-failure',
|
||||
trace: 'on'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "ds",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"~:id": "~ue179d9df-de35-80bf-8005-2861e849b3f7",
|
||||
"~:file-id": "~ue179d9df-de35-80bf-8005-283bbd5516b0",
|
||||
"~:created-at": "~m1729604566293",
|
||||
"~:data": {
|
||||
"~u6ad3e6b9-c5a0-80cf-8005-283bbe38dba8": {
|
||||
"~:id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe38dba8",
|
||||
"~:name": "F",
|
||||
"~:path": "",
|
||||
"~:modified-at": "~m1729604566311",
|
||||
"~:main-instance-id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe378bcc",
|
||||
"~:main-instance-page": "~ue179d9df-de35-80bf-8005-283bbd5516b1"
|
||||
},
|
||||
"~u6ad3e6b9-c5a0-80cf-8005-283bbe39bb51": {
|
||||
"~:id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe39bb51",
|
||||
"~:name": "E",
|
||||
"~:path": "",
|
||||
"~:modified-at": "~m1729604566311",
|
||||
"~:main-instance-id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe378bcd",
|
||||
"~:main-instance-page": "~ue179d9df-de35-80bf-8005-283bbd5516b1"
|
||||
},
|
||||
"~u6ad3e6b9-c5a0-80cf-8005-283bbe3a9014": {
|
||||
"~:id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe3a9014",
|
||||
"~:name": "C",
|
||||
"~:path": "",
|
||||
"~:modified-at": "~m1729604566311",
|
||||
"~:main-instance-id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe378bcf",
|
||||
"~:main-instance-page": "~ue179d9df-de35-80bf-8005-283bbd5516b1"
|
||||
},
|
||||
"~u6ad3e6b9-c5a0-80cf-8005-283bbe3b1793": {
|
||||
"~:id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe3b1793",
|
||||
"~:name": "B",
|
||||
"~:path": "",
|
||||
"~:modified-at": "~m1729604566311",
|
||||
"~:main-instance-id": "~u6ad3e6b9-c5a0-80cf-8005-283bbe378bd0",
|
||||
"~:main-instance-page": "~ue179d9df-de35-80bf-8005-283bbd5516b1"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,6 @@
|
||||
"~:revn": 2,
|
||||
"~:created-at": "~m1730199694953",
|
||||
"~:created-by": "user",
|
||||
"~:profile-id": "~u4678a621-b446-818a-8004-e7b734def799"
|
||||
"~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
"~:revn": 2,
|
||||
"~:created-at": "~m1730199694953",
|
||||
"~:created-by": "user",
|
||||
"~:profile-id": "~u4678a621-b446-818a-8004-e7b734def799"
|
||||
"~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -35,27 +35,61 @@ const setupVariantsFileWithVariant = async (workspacePage) => {
|
||||
|
||||
await workspacePage.clickLeafLayer("Rectangle");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
// We wait until layer-row starts looking like it an component
|
||||
await workspacePage.page.getByTestId("layer-row")
|
||||
.filter({ hasText: "Rectangle"})
|
||||
.getByTestId("icon-component")
|
||||
.waitFor();
|
||||
};
|
||||
|
||||
const findVariant = async (workspacePage, num_variant) => {
|
||||
const container = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ has: workspacePage.page.getByText("Rectangle") })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-component") })
|
||||
.nth(num_variant);
|
||||
const findVariant = async (workspacePage, index) => {
|
||||
const container = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Rectangle"})
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-component") })
|
||||
.nth(index);
|
||||
|
||||
const variant1 = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ has: workspacePage.page.getByText("Value 1") })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-variant") })
|
||||
.nth(num_variant);
|
||||
const variant1 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Value 1" })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-variant") })
|
||||
.nth(index);
|
||||
|
||||
const variant2 = await workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ has: workspacePage.page.getByText("Value 2") })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-variant") })
|
||||
.nth(num_variant);
|
||||
const variant2 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Value 2" })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-variant") })
|
||||
.nth(index);
|
||||
|
||||
await container.waitFor();
|
||||
|
||||
return {
|
||||
container: container,
|
||||
variant1: variant1,
|
||||
variant2: variant2,
|
||||
};
|
||||
};
|
||||
|
||||
const findVariantNoWait = (workspacePage, index) => {
|
||||
const container = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Rectangle"})
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-component") })
|
||||
.nth(index);
|
||||
|
||||
const variant1 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Value 1" })
|
||||
.nth(index);
|
||||
|
||||
const variant2 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: "Value 2" })
|
||||
.nth(index);
|
||||
|
||||
return {
|
||||
container: container,
|
||||
@@ -138,27 +172,33 @@ test("User copy paste a variant container", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupVariantsFileWithVariant(workspacePage);
|
||||
|
||||
const variant = await findVariant(workspacePage, 0);
|
||||
const variant = findVariantNoWait(workspacePage, 0);
|
||||
|
||||
// await variant.container.waitFor();
|
||||
|
||||
// Select the variant container
|
||||
await variant.container.click();
|
||||
|
||||
//Copy the variant container
|
||||
await workspacePage.page.waitForTimeout(1000);
|
||||
|
||||
// Copy the variant container
|
||||
await workspacePage.page.keyboard.press("Control+c");
|
||||
|
||||
//Paste the variant container
|
||||
await workspacePage.clickAt(500, 500);
|
||||
// Paste the variant container
|
||||
await workspacePage.clickAt(400,400);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
|
||||
const variant_original = await findVariant(workspacePage, 1);
|
||||
const variant_duplicate = await findVariant(workspacePage, 0);
|
||||
const variantDuplicate = findVariantNoWait(workspacePage, 0);
|
||||
const variantOriginal = findVariantNoWait(workspacePage, 1);
|
||||
|
||||
// Expand the layers
|
||||
await variant_duplicate.container.getByRole("button").first().click();
|
||||
await variantDuplicate.container.waitFor();
|
||||
await variantDuplicate.container.locator("button").first().click();
|
||||
|
||||
// The variants are valid
|
||||
await validateVariant(variant_original);
|
||||
await validateVariant(variant_duplicate);
|
||||
// // The variants are valid
|
||||
// // await variantOriginal.container.waitFor();
|
||||
await validateVariant(variantOriginal);
|
||||
await validateVariant(variantDuplicate);
|
||||
});
|
||||
|
||||
test("User cut paste a variant container", async ({ page }) => {
|
||||
@@ -172,21 +212,23 @@ test("User cut paste a variant container", async ({ page }) => {
|
||||
|
||||
//Cut the variant container
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
//Paste the variant container
|
||||
await workspacePage.clickAt(500, 500);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
const variant_pasted = await findVariant(workspacePage, 0);
|
||||
const variantPasted = await findVariant(workspacePage, 0);
|
||||
|
||||
// Expand the layers
|
||||
await variant_pasted.container.getByRole("button").first().click();
|
||||
await variantPasted.container.locator("button").first().click();
|
||||
|
||||
// The variants are valid
|
||||
await validateVariant(variant_pasted);
|
||||
await validateVariant(variantPasted);
|
||||
});
|
||||
|
||||
test("[Bugfixing] User cut paste a variant container into a board, and undo twice", async ({
|
||||
test("User cut paste a variant container into a board, and undo twice", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
@@ -205,6 +247,7 @@ test("[Bugfixing] User cut paste a variant container into a board, and undo twic
|
||||
|
||||
//Cut the variant container
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
//Select the board
|
||||
await workspacePage.clickLeafLayer("Board");
|
||||
@@ -215,11 +258,12 @@ test("[Bugfixing] User cut paste a variant container into a board, and undo twic
|
||||
//Undo twice
|
||||
await workspacePage.page.keyboard.press("Control+z");
|
||||
await workspacePage.page.keyboard.press("Control+z");
|
||||
await workspacePage.page.waitForTimeout(500);
|
||||
|
||||
const variant_after_undo = await findVariant(workspacePage, 0);
|
||||
const variantAfterUndo = await findVariant(workspacePage, 0);
|
||||
|
||||
// The variants are valid
|
||||
await validateVariant(variant_after_undo);
|
||||
await validateVariant(variantAfterUndo);
|
||||
});
|
||||
|
||||
test("User copy paste a variant", async ({ page }) => {
|
||||
@@ -364,7 +408,7 @@ test("User drag and drop a component with path inside a variant", async ({
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await setupVariantsFileWithVariant(workspacePage);
|
||||
|
||||
const variant = await findVariant(workspacePage, 0);
|
||||
const variant = findVariantNoWait(workspacePage, 0);
|
||||
|
||||
//Create a component
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
@@ -404,11 +448,12 @@ test("User cut paste a variant into another container", async ({ page }) => {
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
await workspacePage.page.keyboard.press("Control+k");
|
||||
|
||||
const variant_origin = await findVariant(workspacePage, 1);
|
||||
const variant_target = await findVariant(workspacePage, 0);
|
||||
const variantOrigin = await findVariantNoWait(workspacePage, 1);
|
||||
|
||||
// Select the variant1
|
||||
await variant_origin.variant1.click();
|
||||
await variantOrigin.variant1.waitFor();
|
||||
await variantOrigin.variant1.click();
|
||||
await variantOrigin.variant1.click();
|
||||
|
||||
//Cut the variant
|
||||
await workspacePage.page.keyboard.press("Control+x");
|
||||
@@ -417,7 +462,7 @@ test("User cut paste a variant into another container", async ({ page }) => {
|
||||
await workspacePage.layers.getByText("Ellipse").first().click();
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
|
||||
const variant3 = await workspacePage.layers
|
||||
const variant3 = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ has: workspacePage.page.getByText("Value 1, rectangle") })
|
||||
.filter({ has: workspacePage.page.getByTestId("icon-variant") })
|
||||
|
||||
@@ -46,6 +46,9 @@ test("Save and restore version", async ({ page }) => {
|
||||
|
||||
await page.getByLabel("History").click();
|
||||
|
||||
const saveVersionButton = page.getByRole("button", { name: "Save version" });
|
||||
await saveVersionButton.waitFor();
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"create-file-snapshot",
|
||||
"workspace/versions-take-snapshot-1.json",
|
||||
@@ -56,18 +59,22 @@ test("Save and restore version", async ({ page }) => {
|
||||
"workspace/versions-snapshot-2.json",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Save version" }).click();
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file-snapshot",
|
||||
"workspace/versions-update-snapshot-1.json",
|
||||
);
|
||||
|
||||
await saveVersionButton.click();
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-snapshots?file-id=*",
|
||||
"workspace/versions-snapshot-3.json",
|
||||
);
|
||||
|
||||
|
||||
const textbox = page.getByRole("textbox");
|
||||
await textbox.waitFor();
|
||||
|
||||
await page.getByRole("textbox").fill("INIT");
|
||||
await page.getByRole("textbox").press("Enter");
|
||||
|
||||
@@ -76,14 +83,14 @@ test("Save and restore version", async ({ page }) => {
|
||||
.locator("div")
|
||||
.nth(3)
|
||||
.hover();
|
||||
await page.getByRole("button", { name: "Open version menu" }).click();
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"restore-file-snapshot",
|
||||
"workspace/versions-restore-snapshot-1.json",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Open version menu" }).click();
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
|
||||
// check that the history panel is closed after restore
|
||||
|
||||
@@ -248,14 +248,6 @@ test("Bug 9066 - Problem with grid layout", async ({ page }) => {
|
||||
const workspacePage = new WorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile(page);
|
||||
await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-9066.json");
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=e179d9df-de35-80bf-8005-2861e849b3f7",
|
||||
"workspace/get-file-fragment-9066-1.json",
|
||||
);
|
||||
await workspacePage.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=e179d9df-de35-80bf-8005-2861e849785e",
|
||||
"workspace/get-file-fragment-9066-2.json",
|
||||
);
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.storage :as storage]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.string :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
@@ -417,7 +417,7 @@
|
||||
(rx/map (fn [fragment]
|
||||
(assoc cf/public-uri :fragment fragment)))
|
||||
(rx/tap (fn [uri]
|
||||
(wapi/write-to-clipboard (str uri))))
|
||||
(clipboard/to-clipboard (str uri))))
|
||||
(rx/tap on-success)
|
||||
(rx/ignore)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.streams :as ms]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.code-gen.markup-svg :as svg]
|
||||
[app.util.code-gen.style-css :as css]
|
||||
[app.util.globals :as ug]
|
||||
@@ -59,7 +60,6 @@
|
||||
[potok.v2.core :as ptk]
|
||||
[promesa.core :as p]))
|
||||
|
||||
|
||||
(defn copy-selected
|
||||
[]
|
||||
(letfn [(sort-selected [state data]
|
||||
@@ -183,7 +183,7 @@
|
||||
(let [text (wapi/get-current-selected-text)]
|
||||
(if-not (str/empty? text)
|
||||
(try
|
||||
(wapi/write-to-clipboard text)
|
||||
(clipboard/to-clipboard text)
|
||||
(catch :default e
|
||||
(on-copy-error e)))
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(->> (rx/from (clipboard/to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
(rx/map (partial sort-selected state))
|
||||
(rx/map (partial advance-copies state selected))
|
||||
(rx/map #(t/encode-str % {:type :json-verbose}))
|
||||
(rx/map wapi/write-to-clipboard)
|
||||
(rx/map clipboard/to-clipboard)
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))
|
||||
|
||||
@@ -252,49 +252,47 @@
|
||||
(declare ^:private paste-svg-text)
|
||||
(declare ^:private paste-shapes)
|
||||
|
||||
(defn create-paste-from-blob
|
||||
[in-viewport?]
|
||||
(fn [blob]
|
||||
(js/console.log "create-paste-from-blob" blob)
|
||||
(let [type (.-type blob)
|
||||
result (cond
|
||||
(= type "image/svg+xml")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-svg-text))
|
||||
|
||||
(some #(= type %) clipboard/image-types)
|
||||
(rx/of (paste-image blob))
|
||||
|
||||
(= type "text/html")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-html-text))
|
||||
|
||||
(= type "application/transit+json")
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map (fn [text]
|
||||
(let [transit-data (t/decode-str text)]
|
||||
(assoc transit-data :in-viewport in-viewport?))))
|
||||
(rx/map paste-transit-shapes))
|
||||
|
||||
:else
|
||||
(->> (rx/from (.text blob))
|
||||
(rx/map paste-text)))]
|
||||
result)))
|
||||
|
||||
(def default-paste-from-blob (create-paste-from-blob false))
|
||||
|
||||
(defn paste-from-clipboard
|
||||
"Perform a `paste` operation using the Clipboard API."
|
||||
[]
|
||||
(letfn [(decode-entry [entry]
|
||||
(try
|
||||
[:transit (t/decode-str entry)]
|
||||
(catch :default _cause
|
||||
[:text entry])))
|
||||
|
||||
(process-entry [[type data]]
|
||||
(case type
|
||||
:text
|
||||
(cond
|
||||
(str/empty? data)
|
||||
(rx/empty)
|
||||
|
||||
(re-find #"<svg\s" data)
|
||||
(rx/of (paste-svg-text data))
|
||||
|
||||
:else
|
||||
(rx/of (paste-text data)))
|
||||
|
||||
:transit
|
||||
(rx/of (paste-transit-shapes data))))
|
||||
|
||||
(on-error [cause]
|
||||
(let [data (ex-data cause)]
|
||||
(if (:not-implemented data)
|
||||
(rx/of (ntf/warn (tr "errors.clipboard-not-implemented")))
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(ptk/reify ::paste-from-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/concat
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(rx/map decode-entry)
|
||||
(rx/mapcat process-entry))
|
||||
(->> (wapi/read-image-from-clipboard)
|
||||
(rx/map paste-image)))
|
||||
(rx/take 1)
|
||||
(rx/catch on-error))))))
|
||||
(ptk/reify ::paste-from-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(prn "paste-from-clipboard")
|
||||
(->> (clipboard/from-dom-api)
|
||||
(rx/mapcat default-paste-from-blob)
|
||||
(rx/take 1)))))
|
||||
|
||||
(defn paste-from-event
|
||||
"Perform a `paste` operation from user emmited event."
|
||||
@@ -302,6 +300,7 @@
|
||||
(ptk/reify ::paste-from-event
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(prn "paste-from-event")
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
edit-id (dm/get-in state [:workspace-local :edition])
|
||||
is-editing? (and edit-id (= :text (get-in objects [edit-id :type])))]
|
||||
@@ -310,30 +309,8 @@
|
||||
;; we forbid that scenario so the default behaviour is executed
|
||||
(if is-editing?
|
||||
(rx/empty)
|
||||
(let [pdata (wapi/read-from-paste-event event)
|
||||
image-data (some-> pdata wapi/extract-images)
|
||||
text-data (some-> pdata wapi/extract-text)
|
||||
html-data (some-> pdata wapi/extract-html-text)
|
||||
transit-data (ex/ignoring (some-> text-data t/decode-str))]
|
||||
(cond
|
||||
(and (string? text-data) (re-find #"<svg\s" text-data))
|
||||
(rx/of (paste-svg-text text-data))
|
||||
|
||||
(seq image-data)
|
||||
(->> (rx/from image-data)
|
||||
(rx/map paste-image))
|
||||
|
||||
(coll? transit-data)
|
||||
(rx/of (paste-transit-shapes (assoc transit-data :in-viewport in-viewport?)))
|
||||
|
||||
(and (string? html-data) (d/not-empty? html-data))
|
||||
(rx/of (paste-html-text html-data text-data))
|
||||
|
||||
(and (string? text-data) (d/not-empty? text-data))
|
||||
(rx/of (paste-text text-data))
|
||||
|
||||
:else
|
||||
(rx/empty))))))))
|
||||
(->> (clipboard/from-synthetic-event event)
|
||||
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
|
||||
|
||||
(defn copy-selected-svg
|
||||
[]
|
||||
@@ -352,7 +329,7 @@
|
||||
|
||||
shapes (mapv maybe-translate selected)
|
||||
svg-formatted (svg/generate-formatted-markup objects shapes)]
|
||||
(wapi/write-to-clipboard svg-formatted)))))
|
||||
(clipboard/to-clipboard svg-formatted)))))
|
||||
|
||||
(defn copy-selected-css
|
||||
[]
|
||||
@@ -362,7 +339,7 @@
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
selected (->> (dsh/lookup-selected state) (mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
(clipboard/to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-css-nested
|
||||
[]
|
||||
@@ -374,7 +351,7 @@
|
||||
(cfh/selected-with-children objects)
|
||||
(mapv (d/getf objects)))
|
||||
css (css/generate-style objects selected selected {:with-prelude? false})]
|
||||
(wapi/write-to-clipboard css)))))
|
||||
(clipboard/to-clipboard css)))))
|
||||
|
||||
(defn copy-selected-text
|
||||
[]
|
||||
@@ -405,7 +382,7 @@
|
||||
(-> shape :content txt/content->text))))
|
||||
(str/join "\n"))]
|
||||
|
||||
(wapi/write-to-clipboard text)))))
|
||||
(clipboard/to-clipboard text)))))
|
||||
|
||||
(defn copy-selected-props
|
||||
[]
|
||||
@@ -474,7 +451,7 @@
|
||||
(rx/map #(wapi/create-blob % "text/plain"))
|
||||
(rx/subs! resolve reject))))]
|
||||
|
||||
(->> (rx/from (wapi/write-to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(->> (rx/from (clipboard/to-clipboard-promise "text/plain" resolve-data-promise))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore)))
|
||||
;; FIXME: this is to support Firefox versions below 116 that don't support
|
||||
@@ -482,7 +459,7 @@
|
||||
;; https://caniuse.com/?search=ClipboardItem
|
||||
(->> (rx/of copy-data)
|
||||
(rx/mapcat resolve-images)
|
||||
(rx/map #(wapi/write-to-clipboard (t/encode-str % {:type :json-verbose})))
|
||||
(rx/map #(clipboard/to-clipboard (t/encode-str % {:type :json-verbose})))
|
||||
(rx/catch on-copy-error)
|
||||
(rx/ignore))))))))))))
|
||||
|
||||
@@ -502,7 +479,8 @@
|
||||
(js/console.error "Clipboard error:" cause))
|
||||
(rx/empty)))]
|
||||
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(->> (clipboard/from-dom-api)
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/map decode-entry)
|
||||
(rx/take 1)
|
||||
(rx/catch on-error)))))))
|
||||
@@ -968,17 +946,21 @@
|
||||
(deref ms/mouse-position)))
|
||||
|
||||
(defn- paste-html-text
|
||||
[html text]
|
||||
[html]
|
||||
(js/console.log html)
|
||||
(dm/assert! (string? html))
|
||||
(ptk/reify ::paste-html-text
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [style (deref refs/workspace-clipboard-style)
|
||||
root (dwtxt/create-root-from-html html style)
|
||||
text (.-textContent root)
|
||||
content (tc/dom->cljs root)]
|
||||
(js/console.log "root" root "content" content)
|
||||
(when (types.text/valid-content? content)
|
||||
(js/console.log "valid-content")
|
||||
(let [id (uuid/next)
|
||||
width (max 8 (min (* 7 (count text)) 700))
|
||||
width (max 8 (min (* 7 (count text)) 700))
|
||||
height 16
|
||||
{:keys [x y]} (calculate-paste-position state)
|
||||
|
||||
@@ -1051,4 +1033,4 @@
|
||||
(ptk/reify ::copy-link-to-clipboard
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(wapi/write-to-clipboard (rt/get-current-href)))))
|
||||
(clipboard/to-clipboard (rt/get-current-href)))))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.debug.icons-preview :refer [icons-preview]]
|
||||
[app.main.ui.debug.playground :refer [playground]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.error-boundary :refer [error-boundary*]]
|
||||
[app.main.ui.exports.files]
|
||||
@@ -209,6 +210,10 @@
|
||||
(when *assert*
|
||||
[:& icons-preview])
|
||||
|
||||
:debug-playground
|
||||
(when *assert*
|
||||
[:& playground])
|
||||
|
||||
(:dashboard-search
|
||||
:dashboard-recent
|
||||
:dashboard-files
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc copy-button*
|
||||
@@ -34,7 +34,7 @@
|
||||
(reset! active* true)
|
||||
(tm/schedule 1000 #(reset! active* false))
|
||||
(when (fn? on-copied) (on-copied event))
|
||||
(wapi/write-to-clipboard
|
||||
(clipboard/to-clipboard
|
||||
(if (fn? data) (data) data)))))]
|
||||
|
||||
[:button {:class class
|
||||
|
||||
51
frontend/src/app/main/ui/debug/playground.cljs
Normal file
51
frontend/src/app/main/ui/debug/playground.cljs
Normal file
@@ -0,0 +1,51 @@
|
||||
(ns app.main.ui.debug.playground
|
||||
#_(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.util.clipboard :as clipboard]
|
||||
[beicon.v2.core :as rx]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc playground-clipboard
|
||||
{::mf/wrap-props false
|
||||
::mf/private true}
|
||||
[]
|
||||
(let [on-paste (mf/use-fn
|
||||
(fn [e]
|
||||
(let [stream (clipboard/from-event e)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))
|
||||
|
||||
on-dragover (mf/use-fn
|
||||
(fn [e]
|
||||
(.preventDefault e)))
|
||||
|
||||
on-drop (mf/use-fn
|
||||
(fn [e]
|
||||
(.preventDefault e)
|
||||
(let [stream (clipboard/from-drop-event e)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))
|
||||
|
||||
on-click (mf/use-fn
|
||||
(fn [e]
|
||||
(js/console.log "event" e)
|
||||
(let [stream (clipboard/from-dom-api)]
|
||||
(rx/sub! stream
|
||||
(fn [data]
|
||||
(js/console.log "data" data))))))]
|
||||
|
||||
(.addEventListener js/window "paste" on-paste)
|
||||
(.addEventListener js/window "drop" on-drop)
|
||||
(.addEventListener js/window "dragover" on-dragover)
|
||||
|
||||
[:button#paste {:on-click on-click} "Paste"]))
|
||||
|
||||
(mf/defc playground
|
||||
{::mf/wrap-props false
|
||||
::mf/private true}
|
||||
[]
|
||||
[:& playground-clipboard])
|
||||
|
||||
|
||||
0
frontend/src/app/main/ui/debug/playground.scss
Normal file
0
frontend/src/app/main/ui/debug/playground.scss
Normal file
@@ -23,11 +23,11 @@
|
||||
[app.main.ui.hooks.resize :refer [use-resize-hook]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.code-beautify :as cb]
|
||||
[app.util.code-gen :as cg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
@@ -202,7 +202,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps style-code markup-code images-data)
|
||||
(fn []
|
||||
(wapi/write-to-clipboard (gen-all-code style-code markup-code images-data))
|
||||
(clipboard/to-clipboard (gen-all-code style-code markup-code images-data))
|
||||
(let [origin (if (= :workspace from)
|
||||
"workspace"
|
||||
"viewer")]
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.main.ui.inspect.styles.rows.color-properties-row :refer [color-properties-row*]]
|
||||
[app.main.ui.inspect.styles.rows.properties-row :refer [properties-row*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
(.toUpperCase text)
|
||||
text)]
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(clipboard/to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))
|
||||
composite-typography-token (get-resolved-token :typography shape resolved-tokens)]
|
||||
[:div {:class (stl/css :text-properties)}
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.color :as uc]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
(mf/deps copied formatted-color-value)
|
||||
(fn []
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(clipboard/to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:*
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
format-token-value]]
|
||||
[app.main.ui.ds.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
(mf/deps copied)
|
||||
(fn []
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard copiable-value)
|
||||
(clipboard/to-clipboard copiable-value)
|
||||
(tm/schedule 1000 #(reset! copied* false))))]
|
||||
[:dl {:class [(stl/css :property-row) class]
|
||||
:data-testid "property-row"}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
[app.common.data :as d]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- panel->title
|
||||
@@ -50,7 +50,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps shorthand)
|
||||
(fn []
|
||||
(wapi/write-to-clipboard (str shorthand))))]
|
||||
(clipboard/to-clipboard (str shorthand))))]
|
||||
[:article {:class (stl/css :style-box)}
|
||||
[:header {:class (stl/css :disclosure-header)}
|
||||
[:button {:class (stl/css :disclosure-button)
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
(when *assert*
|
||||
["/debug/icons-preview" :debug-icons-preview])
|
||||
|
||||
(when *assert*
|
||||
["/debug/playground" :debug-playground])
|
||||
|
||||
;; Used for export
|
||||
["/render-sprite/:file-id" :render-sprite]
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.webapi :as wapi]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
(mf/deps created)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(wapi/write-to-clipboard (:token created))
|
||||
(clipboard/to-clipboard (:token created))
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "dashboard.access-tokens.copied-success")
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.webapi :as wapi]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
copy-link
|
||||
(fn [_]
|
||||
(wapi/write-to-clipboard current-link)
|
||||
(clipboard/to-clipboard current-link)
|
||||
(st/emit! (ntf/show {:level :info
|
||||
:type :toast
|
||||
:content (tr "common.share-link.link-copied-success")
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.workspace.sidebar.assets.common :as cmm]
|
||||
[app.util.clipboard :as clipboard]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr] :as i18n]
|
||||
[app.util.shape-icon :as usi]
|
||||
[app.util.timers :as timers]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[okulary.core :as l]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -181,7 +181,9 @@
|
||||
handle-hover-copy-paste
|
||||
(mf/use-callback
|
||||
(fn []
|
||||
(->> (wapi/read-from-clipboard)
|
||||
(->> (clipboard/from-dom-api)
|
||||
;: FIXME: use specific API for access .text
|
||||
(rx/mapcat #(.text %))
|
||||
(rx/take 1)
|
||||
(rx/subs!
|
||||
(fn [data]
|
||||
|
||||
161
frontend/src/app/util/clipboard.cljs
Normal file
161
frontend/src/app/util/clipboard.cljs
Normal file
@@ -0,0 +1,161 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.util.clipboard
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.transit :as t]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]))
|
||||
|
||||
(def max-parseable-size
|
||||
(* 16 1024 1024))
|
||||
|
||||
(def image-types
|
||||
["image/webp"
|
||||
"image/png"
|
||||
"image/jpeg"
|
||||
"image/svg+xml"])
|
||||
|
||||
(def allowed-types
|
||||
(d/ordered-set
|
||||
"image/webp",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain"))
|
||||
|
||||
(def exclusive-types
|
||||
(d/ordered-set
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain"))
|
||||
|
||||
(def clipboard-settings
|
||||
#js {:decodeTransit t/decode-str})
|
||||
|
||||
(defn- parse-pain-text
|
||||
[text]
|
||||
(or (when (ex/ignoring (t/decode-str text))
|
||||
(new js/Blob #js [text] #js {:type "application/transit+json"}))
|
||||
(when (re-seq #"^<svg[\s>]" text)
|
||||
(new js/Blob #js [text] #js {:type "image/svg+xml"}))
|
||||
(new js/Blob #js [text] #js {:type "text/plain"})))
|
||||
|
||||
(defn from-dom-api
|
||||
[]
|
||||
(let [api (.-clipboard js/navigator)]
|
||||
(->> (rx/from (.read ^js api))
|
||||
(rx/mapcat (comp rx/from obj/into-array))
|
||||
(rx/mapcat (fn [item]
|
||||
(let [allowed-types'
|
||||
(->> (seq (.-types item))
|
||||
(filter (fn [type] (contains? allowed-types type)))
|
||||
(sort-by (fn [type] (d/index-of allowed-types type)))
|
||||
(into (d/ordered-set)))
|
||||
|
||||
main-type
|
||||
(first allowed-types')]
|
||||
|
||||
(cond->> (rx/from (.getType ^js item main-type))
|
||||
(and (= (count allowed-types') 1)
|
||||
(= "text/plain" main-type))
|
||||
(rx/mapcat (fn [blob]
|
||||
(if (>= max-parseable-size (.-size ^js blob))
|
||||
(->> (rx/from (.text ^js blob))
|
||||
(rx/map parse-pain-text))
|
||||
(rx/of blob)))))))))))
|
||||
|
||||
(defn- from-data-transfer
|
||||
"Get clipboard stream from DataTransfer instance"
|
||||
[data-transfer]
|
||||
(let [sorted-items
|
||||
(->> (seq (.-items ^js data-transfer))
|
||||
(filter (fn [item]
|
||||
(contains? allowed-types (.-type ^js item))))
|
||||
(sort-by (fn [item] (d/index-of allowed-types (.-type item)))))]
|
||||
(->> (rx/from sorted-items)
|
||||
(rx/mapcat (fn [item]
|
||||
(let [kind (.-kind ^js item)
|
||||
type (.-type ^js item)]
|
||||
(cond
|
||||
(= kind "file")
|
||||
(rx/of (.getAsFile ^js item))
|
||||
|
||||
(= kind "string")
|
||||
(->> (rx/create (fn [subs]
|
||||
(.getAsString ^js item
|
||||
(fn [text]
|
||||
(rx/push! subs (d/vec2 type text))
|
||||
(rx/end! subs)))))
|
||||
(rx/map (fn [[type text]]
|
||||
(if (= type "text/plain")
|
||||
(parse-pain-text text)
|
||||
(new js/Blob #js [text] #js {:type type})))))
|
||||
:else
|
||||
(rx/empty)))))
|
||||
(rx/filter some?)
|
||||
(rx/reduce (fn [filtered item]
|
||||
(js/console.log "AAA" item)
|
||||
(let [type (.-type ^js item)]
|
||||
(if (and (contains? exclusive-types type)
|
||||
(some (fn [item] (contains? exclusive-types type)) filtered))
|
||||
filtered
|
||||
(conj filtered item))))
|
||||
(d/ordered-set))
|
||||
(rx/mapcat (comp rx/from seq)))))
|
||||
|
||||
(defn from-event
|
||||
"Get clipboard stream from event"
|
||||
[event]
|
||||
(let [cdata (.-clipboardData ^js event)]
|
||||
(from-data-transfer cdata)))
|
||||
|
||||
(defn from-synthetic-event
|
||||
"Get clipboard stream from syntetic event"
|
||||
[event]
|
||||
(let [target
|
||||
(dom/get-target event)
|
||||
|
||||
content-editable?
|
||||
(dom/is-content-editable? target)
|
||||
|
||||
is-input?
|
||||
(= (dom/get-tag-name target) "INPUT")]
|
||||
|
||||
;; ignore when pasting into an editable control
|
||||
(when-not (or content-editable? is-input?)
|
||||
(-> event
|
||||
(dom/event->browser-event)
|
||||
(from-event)))))
|
||||
|
||||
(defn from-drop-event
|
||||
"Get clipboard stream from drop event"
|
||||
[event]
|
||||
(from-data-transfer (.-dataTransfer ^js event)))
|
||||
|
||||
;; FIXME: rename to `write-text`
|
||||
(defn to-clipboard
|
||||
[data]
|
||||
(assert (string? data) "`data` should be string")
|
||||
(let [clipboard (unchecked-get js/navigator "clipboard")]
|
||||
(.writeText ^js clipboard data)))
|
||||
|
||||
(defn- create-clipboard-item
|
||||
[mimetype promise]
|
||||
(js/ClipboardItem.
|
||||
(js-obj mimetype promise)))
|
||||
|
||||
;; FIXME: this API is very confuse
|
||||
(defn to-clipboard-promise
|
||||
[mimetype promise]
|
||||
(let [clipboard (unchecked-get js/navigator "clipboard")
|
||||
data (create-clipboard-item mimetype promise)]
|
||||
(.write ^js clipboard #js [data])))
|
||||
151
frontend/src/app/util/clipboard.js
Normal file
151
frontend/src/app/util/clipboard.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
const maxParseableSize = 16 * 1024 * 1024;
|
||||
|
||||
const allowedTypes = [
|
||||
"image/webp",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/svg+xml",
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
const exclusiveTypes = [
|
||||
"application/transit+json",
|
||||
"text/html",
|
||||
"text/plain"
|
||||
];
|
||||
|
||||
/**
|
||||
* @typedef {Object} ClipboardSettings
|
||||
* @property {Function} [decodeTransit]
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {ClipboardSettings} options
|
||||
* @param {Blob} [defaultReturn]
|
||||
* @returns {Blob}
|
||||
*/
|
||||
function parseClipboardItemText(
|
||||
text,
|
||||
options,
|
||||
defaultReturn = new Blob([text], { type: "text/plain" }),
|
||||
) {
|
||||
let decodedTransit = false;
|
||||
try { decodedTransit = options?.decodeTransit?.(text) ?? false }
|
||||
catch (error) { /* NOOP */ }
|
||||
if (/^<svg[\s>]/i.test(text)) {
|
||||
return new Blob([text], { type: "image/svg+xml" });
|
||||
} else if (decodedTransit) {
|
||||
return new Blob([text], { type: "application/transit+json" });
|
||||
}
|
||||
return defaultReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromClipboard(options) {
|
||||
const items = await navigator.clipboard.read();
|
||||
console.log("items", items);
|
||||
return Promise.all(
|
||||
Array.from(items).map(async (item) => {
|
||||
const itemAllowedTypes = Array.from(item.types)
|
||||
.filter((type) => allowedTypes.includes(type))
|
||||
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b));
|
||||
|
||||
if (
|
||||
itemAllowedTypes.length === 1 &&
|
||||
itemAllowedTypes.at(0) === "text/plain"
|
||||
) {
|
||||
const blob = await item.getType("text/plain");
|
||||
if (blob.size < maxParseableSize) {
|
||||
const text = await blob.text();
|
||||
return parseClipboardItemText(text, options);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
const type = itemAllowedTypes.at(0);
|
||||
return item.getType(type);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DataTransfer} dataTransfer
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export async function fromDataTransfer(dataTransfer, options) {
|
||||
const items = await Promise.all(
|
||||
Array.from(dataTransfer.items)
|
||||
.filter((item) => allowedTypes.includes(item.type))
|
||||
.sort(
|
||||
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
|
||||
)
|
||||
.map(async (item) => {
|
||||
if (item.kind === "file") {
|
||||
return Promise.resolve(item.getAsFile());
|
||||
} else if (item.kind === "string") {
|
||||
return new Promise((resolve) => {
|
||||
const type = item.type;
|
||||
item.getAsString((text) => {
|
||||
if (type === "text/plain") {
|
||||
return resolve(parseClipboardItemText(text, options));
|
||||
}
|
||||
return resolve(new Blob([text], { type }));
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}),
|
||||
);
|
||||
return items
|
||||
.filter((item) => !!item)
|
||||
.reduce((filtered, item) => {
|
||||
if (
|
||||
exclusiveTypes.includes(item.type) &&
|
||||
filtered.find((filteredItem) =>
|
||||
exclusiveTypes.includes(filteredItem.type),
|
||||
)
|
||||
) {
|
||||
return filtered;
|
||||
}
|
||||
filtered.push(item);
|
||||
return filtered;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} clipboardData
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export function fromClipboardData(clipboardData, options) {
|
||||
return fromDataTransfer(clipboardData, options);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e
|
||||
* @param {ClipboardSettings} [options]
|
||||
* @returns {Promise<Array<Blob>>}
|
||||
*/
|
||||
export function fromClipboardEvent(e, options) {
|
||||
return fromClipboardData(e.clipboardData, options);
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
(extend-type BrowserEvent
|
||||
cljs.core/IDeref
|
||||
(-deref [it] (.getBrowserEvent it)))
|
||||
(-deref [it] (.getBrowserEvent ^js it)))
|
||||
|
||||
(declare get-window-size)
|
||||
|
||||
@@ -360,6 +360,10 @@
|
||||
(when (some? el)
|
||||
(.-innerText el)))
|
||||
|
||||
(defn is-content-editable?
|
||||
[^js el]
|
||||
(.-isContentEditable ^js el))
|
||||
|
||||
(defn query
|
||||
([^string selector]
|
||||
(query globals/document selector))
|
||||
|
||||
@@ -37,34 +37,36 @@
|
||||
(.-textContent element)))
|
||||
|
||||
(defn get-attrs-from-styles
|
||||
[element attrs]
|
||||
[element attrs defaults]
|
||||
(reduce (fn [acc key]
|
||||
(let [style (.-style element)]
|
||||
(if (contains? styles/mapping key)
|
||||
(let [style-name (styles/get-style-name-as-css-variable key)
|
||||
[_ style-decode] (get styles/mapping key)
|
||||
value (style-decode (.getPropertyValue style style-name))]
|
||||
(assoc acc key value))
|
||||
(let [style-name (styles/get-style-name key)]
|
||||
(assoc acc key (styles/normalize-attr-value key (.getPropertyValue style style-name))))))) {} attrs))
|
||||
(assoc acc key (if (empty? value) (get defaults key) value)))
|
||||
(let [style-name (styles/get-style-name key)
|
||||
value (styles/normalize-attr-value key (.getPropertyValue style style-name))]
|
||||
(assoc acc key (if (empty? value) (get defaults key) value)))))) {} attrs))
|
||||
|
||||
(defn get-inline-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/text-node-attrs))
|
||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
||||
|
||||
(defn get-paragraph-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element (d/concat-set txt/paragraph-attrs txt/text-node-attrs)))
|
||||
(get-attrs-from-styles element (d/concat-set txt/paragraph-attrs txt/text-node-attrs) (d/merge txt/default-paragraph-attrs txt/default-text-attrs)))
|
||||
|
||||
(defn get-root-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/root-attrs))
|
||||
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
||||
|
||||
(defn create-inline
|
||||
[element]
|
||||
(d/merge {:text (get-inline-text element)
|
||||
:key (.-id element)}
|
||||
(get-inline-styles element)))
|
||||
(let [text (get-inline-text element)]
|
||||
(d/merge {:text text
|
||||
:key (.-id element)}
|
||||
(get-inline-styles element))))
|
||||
|
||||
(defn create-paragraph
|
||||
[element]
|
||||
@@ -76,7 +78,7 @@
|
||||
(defn create-root
|
||||
[element]
|
||||
(let [root-styles (get-root-styles element)]
|
||||
(d/merge {:type "root",
|
||||
(d/merge {:type "root"
|
||||
:key (.-id element)
|
||||
:children [{:type "paragraph-set"
|
||||
:children (mapv create-paragraph (.-children element))}]}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.util.webapi
|
||||
"HTML5 web api helpers."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as log]
|
||||
[app.util.globals :as globals]
|
||||
@@ -115,68 +114,6 @@
|
||||
[]
|
||||
(.. js/window getSelection toString))
|
||||
|
||||
(defn write-to-clipboard
|
||||
[data]
|
||||
(assert (string? data) "`data` should be string")
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")]
|
||||
(.writeText ^js cboard data)))
|
||||
|
||||
(defn write-to-clipboard-promise
|
||||
[mimetype promise]
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")
|
||||
data (js/ClipboardItem.
|
||||
(-> (obj/create)
|
||||
(obj/set! mimetype promise)))]
|
||||
(.write ^js cboard #js [data])))
|
||||
|
||||
(defn read-from-clipboard
|
||||
[]
|
||||
(try
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")]
|
||||
(if (.-readText ^js cboard)
|
||||
(rx/from (.readText ^js cboard))
|
||||
(rx/throw (ex-info "This browser does not implement read from clipboard protocol"
|
||||
{:not-implemented true}))))
|
||||
(catch :default cause
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn read-image-from-clipboard
|
||||
[]
|
||||
(try
|
||||
(let [cboard (unchecked-get js/navigator "clipboard")
|
||||
read-item (fn [item]
|
||||
(let [img-type (->> (.-types ^js item)
|
||||
(d/seek #(str/starts-with? % "image/")))]
|
||||
(if img-type
|
||||
(rx/from (.getType ^js item img-type))
|
||||
(rx/empty))))]
|
||||
(->> (rx/from (.read ^js cboard)) ;; Get a stream of item lists
|
||||
(rx/mapcat identity) ;; Convert each item into an emission
|
||||
(rx/switch-map read-item)))
|
||||
(catch :default cause
|
||||
(rx/throw cause))))
|
||||
|
||||
(defn read-from-paste-event
|
||||
[event]
|
||||
(let [target (.-target ^js event)]
|
||||
(when (and (not (.-isContentEditable ^js target)) ;; ignore when pasting into
|
||||
(not= (.-tagName ^js target) "INPUT")) ;; an editable control
|
||||
(.. ^js event getBrowserEvent -clipboardData))))
|
||||
|
||||
(defn extract-html-text
|
||||
[clipboard-data]
|
||||
(.getData clipboard-data "text/html"))
|
||||
|
||||
(defn extract-text
|
||||
[clipboard-data]
|
||||
(.getData clipboard-data "text"))
|
||||
|
||||
(defn extract-images
|
||||
"Get image files from clipboard data. Returns a native js array."
|
||||
[clipboard-data]
|
||||
(let [files (obj/into-array (.-files ^js clipboard-data))]
|
||||
(.filter ^js files #(str/starts-with? (obj/get % "type") "image/"))))
|
||||
|
||||
(defn create-canvas-element
|
||||
[width height]
|
||||
(let [canvas (.createElement js/document "canvas")]
|
||||
|
||||
@@ -4352,7 +4352,7 @@ __metadata:
|
||||
npm-run-all: "npm:^4.1.5"
|
||||
opentype.js: "npm:^1.3.4"
|
||||
p-limit: "npm:^6.2.0"
|
||||
playwright: "npm:1.52.0"
|
||||
playwright: "npm:1.56.1"
|
||||
postcss: "npm:^8.5.4"
|
||||
postcss-clean: "npm:^1.2.2"
|
||||
postcss-modules: "npm:^6.0.1"
|
||||
|
||||
Reference in New Issue
Block a user