♻️ Refactor clipboard

This commit is contained in:
Aitor Moreno
2025-11-07 14:20:08 +01:00
committed by Andrey Antukh
parent 9532dea2c6
commit 32e1b55658
20 changed files with 364 additions and 172 deletions

View File

@@ -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"

View File

@@ -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))))))

View File

@@ -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,45 @@
(declare ^:private paste-svg-text)
(declare ^:private paste-shapes)
(defn create-paste-from-blob
[in-viewport?]
(fn [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))))))
(->> (clipboard/from-clipboard)
(rx/mapcat default-paste-from-blob)
(rx/take 1)))))
(defn paste-from-event
"Perform a `paste` operation from user emmited event."
@@ -310,30 +306,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-clipboard-event event)
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
(defn copy-selected-svg
[]
@@ -352,7 +326,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 +336,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 +348,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 +379,7 @@
(-> shape :content txt/content->text))))
(str/join "\n"))]
(wapi/write-to-clipboard text)))))
(clipboard/to-clipboard text)))))
(defn copy-selected-props
[]
@@ -474,7 +448,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 +456,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 +476,8 @@
(js/console.error "Clipboard error:" cause))
(rx/empty)))]
(->> (wapi/read-from-clipboard)
(->> (clipboard/from-clipboard)
(rx/mapcat #(.text %))
(rx/map decode-entry)
(rx/take 1)
(rx/catch on-error)))))))
@@ -968,15 +943,19 @@
(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))
height 16
@@ -1051,4 +1030,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)))))

View File

@@ -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

View File

@@ -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

View 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-clipboard-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-clipboard)]
(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])

View 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")]

View File

@@ -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)}

View File

@@ -15,10 +15,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]

View File

@@ -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"}

View File

@@ -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)

View File

@@ -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]

View File

@@ -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")

View File

@@ -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")

View File

@@ -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,8 @@
handle-hover-copy-paste
(mf/use-callback
(fn []
(->> (wapi/read-from-clipboard)
(->> (clipboard/from-clipboard)
(rx/mapcat #(.text %))
(rx/take 1)
(rx/subs!
(fn [data]

View File

@@ -0,0 +1,59 @@
;; 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
["./clipboard.js" :as clipboard]
[app.common.transit :as t]
[beicon.v2.core :as rx]))
(def image-types
["image/webp"
"image/png"
"image/jpeg"
"image/svg+xml"])
(def clipboard-settings #js {:decodeTransit t/decode-str})
(defn from-clipboard []
(->> (rx/from (clipboard/fromClipboard clipboard-settings))
(rx/mapcat #(rx/from %))))
(defn from-data-transfer [data-transfer]
(->> (rx/from (clipboard/fromDataTransfer data-transfer clipboard-settings))
(rx/mapcat #(rx/from %))))
(defn from-clipboard-data [clipboard-data]
(from-data-transfer clipboard-data))
(defn from-clipboard-event [event]
(from-clipboard-data (.-clipboardData event)))
(defn from-synthetic-clipboard-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
(from-clipboard-event (. ^js event getBrowserEvent)))))
(defn from-drop-event [event]
(from-data-transfer (.-dataTransfer event)))
(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)))
(defn to-clipboard-promise
[mimetype promise]
(let [clipboard (unchecked-get js/navigator "clipboard")
data (create-clipboard-item mimetype promise)]
(.write ^js clipboard #js [data])))

View 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);
}

View File

@@ -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)
(let [text (get-inline-text element)]
(d/merge {:text text
:key (.-id element)}
(get-inline-styles 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))}]}

View File

@@ -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")]