mirror of
https://github.com/penpot/penpot.git
synced 2026-01-14 01:09:51 -05:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef3588d05f | ||
|
|
d4893523bc | ||
|
|
f10792619d | ||
|
|
7a0702650a | ||
|
|
ee1230c488 | ||
|
|
ede1176606 | ||
|
|
9506606e15 | ||
|
|
7e5f93ca3d | ||
|
|
6655563aba | ||
|
|
8ee9b45243 | ||
|
|
caa6897f81 | ||
|
|
660bc1a4dd | ||
|
|
3ddd45e99b | ||
|
|
9b71e04e1c | ||
|
|
39620fe9c4 | ||
|
|
db7c1fc7dd | ||
|
|
c89abf56ac | ||
|
|
d22f6e37c9 | ||
|
|
19b9b3cbd9 | ||
|
|
c1d3e4cd6e | ||
|
|
2164593757 | ||
|
|
9485ce03b5 | ||
|
|
ba832389d1 | ||
|
|
a8ee9be7b9 | ||
|
|
c8c83c1e1d | ||
|
|
afcfbdedda | ||
|
|
fa8665df88 | ||
|
|
5cc678ddc3 | ||
|
|
64c8741233 | ||
|
|
0cae9d6ad5 | ||
|
|
0c586551c4 | ||
|
|
2f4cb19745 | ||
|
|
b80ccbec0f | ||
|
|
246415be2b | ||
|
|
7faa9e970e | ||
|
|
04a0d867b0 | ||
|
|
a18214a1a5 | ||
|
|
68397edd4d | ||
|
|
1e2d9a15a3 | ||
|
|
0f101fad9f | ||
|
|
a91737b4d7 | ||
|
|
284d5ecb77 | ||
|
|
5d95d755ad | ||
|
|
4466abd150 | ||
|
|
27690c3da6 | ||
|
|
f436d72f51 | ||
|
|
20ea188070 | ||
|
|
c4f076910b | ||
|
|
72f2395142 | ||
|
|
47d28758d7 | ||
|
|
b7573c0b72 | ||
|
|
2ed743b6be | ||
|
|
036e335fc4 | ||
|
|
0e99b37c21 | ||
|
|
3cdbd7f381 | ||
|
|
76caff2b61 | ||
|
|
bb370b3e50 | ||
|
|
45d56f40e1 | ||
|
|
4a1ab75d8f | ||
|
|
a58ad2298a |
@@ -33,6 +33,12 @@ jobs:
|
||||
command: |
|
||||
clojure -M:dev:test
|
||||
|
||||
- run:
|
||||
name: "NODE tests"
|
||||
working_directory: "./common"
|
||||
command: |
|
||||
yarn run test
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.m2
|
||||
|
||||
@@ -24,17 +24,20 @@
|
||||
### :sparkles: New features
|
||||
|
||||
- Viewer role for team members [Taiga #1056 & #6590](https://tree.taiga.io/project/penpot/us/1056 & https://tree.taiga.io/project/penpot/us/6590)
|
||||
- File history versions management [Taiga](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
|
||||
- File history versions management [Taiga #187](https://tree.taiga.io/project/penpot/us/187?milestone=411120)
|
||||
- Rename selected layer via keyboard shortcut and context menu option [Taiga #8882](https://tree.taiga.io/project/penpot/us/8882)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem with some texts desynchronization [Taiga #9379](https://tree.taiga.io/project/penpot/issue/9379)
|
||||
|
||||
## 2.3.3
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix problem creating manual overlay interactions [Taiga #9146](https://tree.taiga.io/project/penpot/issue/9146)
|
||||
- Fix plugins list default URL
|
||||
- Activate plugins feature by default
|
||||
|
||||
## 2.3.2
|
||||
|
||||
|
||||
@@ -134,6 +134,16 @@
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))))))))
|
||||
|
||||
(defn clean-file-features
|
||||
[file]
|
||||
(update file :features (fn [features]
|
||||
(if (set? features)
|
||||
(-> features
|
||||
(cfeat/migrate-legacy-features)
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/difference cfeat/backend-only-features))
|
||||
#{}))))
|
||||
|
||||
(defn get-project
|
||||
[cfg project-id]
|
||||
(db/get cfg :project {:id project-id}))
|
||||
@@ -445,8 +455,11 @@
|
||||
(fn [features]
|
||||
(let [features (cfeat/check-supported-features! features)]
|
||||
(-> (::features cfg #{})
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union features))))))
|
||||
(set/union features)
|
||||
;; We never want to store
|
||||
;; frontend-only features on file
|
||||
(set/difference cfeat/frontend-only-features))))))
|
||||
|
||||
|
||||
_ (when (contains? cf/flags :file-schema-validation)
|
||||
(fval/validate-file-schema! file))
|
||||
|
||||
@@ -508,15 +508,6 @@
|
||||
(update :object-id #(str/replace-first % #"^(.*?)/" (str file-id "/")))))
|
||||
thumbnails))
|
||||
|
||||
(defn- clean-features
|
||||
[file]
|
||||
(update file :features (fn [features]
|
||||
(if (set? features)
|
||||
(-> features
|
||||
(cfeat/migrate-legacy-features)
|
||||
(set/difference cfeat/backend-only-features))
|
||||
#{}))))
|
||||
|
||||
(defmethod read-section :v1/files
|
||||
[{:keys [::db/conn ::input ::project-id ::bfc/overwrite ::name] :as system}]
|
||||
|
||||
@@ -527,7 +518,7 @@
|
||||
file-id (:id file)
|
||||
file-id' (bfc/lookup-index file-id)
|
||||
|
||||
file (clean-features file)
|
||||
file (bfc/clean-file-features file)
|
||||
thumbnails (:thumbnails file)]
|
||||
|
||||
(when (not= file-id expected-file-id)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
@@ -55,7 +56,8 @@
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:project-id ::sm/uuid]]]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:features ::cfeat/features]]]]
|
||||
|
||||
[:relations {:optional true}
|
||||
[:vector
|
||||
@@ -203,7 +205,10 @@
|
||||
(dissoc :libraries))
|
||||
|
||||
embed-assets
|
||||
(update :data #(bfc/embed-assets cfg % file-id)))))
|
||||
(update :data #(bfc/embed-assets cfg % file-id))
|
||||
|
||||
:always
|
||||
(bfc/clean-file-features))))
|
||||
|
||||
(defn- resolve-extension
|
||||
[mtype]
|
||||
@@ -259,7 +264,8 @@
|
||||
(vswap! bfc/*state* update :files assoc file-id
|
||||
{:id file-id
|
||||
:project-id (:project-id file)
|
||||
:name (:name file)})
|
||||
:name (:name file)
|
||||
:features (:features file)})
|
||||
|
||||
(let [file (cond-> (dissoc file :data)
|
||||
(:options data)
|
||||
|
||||
@@ -144,6 +144,8 @@
|
||||
[:quotes-comments-per-file {:optional true} ::sm/int]
|
||||
[:quotes-snapshots-per-file {:optional true} ::sm/int]
|
||||
[:quotes-snapshots-per-team {:optional true} ::sm/int]
|
||||
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
|
||||
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
|
||||
|
||||
[:auth-data-cookie-domain {:optional true} :string]
|
||||
[:auth-token-cookie-name {:optional true} :string]
|
||||
|
||||
@@ -226,8 +226,8 @@
|
||||
[:priority {:optional true} [:enum :high :low]]
|
||||
[:extra-data {:optional true} ::sm/text]])
|
||||
|
||||
(def ^:private valid-context?
|
||||
(sm/validator schema:context))
|
||||
(def ^:private check-context
|
||||
(sm/check-fn schema:context))
|
||||
|
||||
(defn template-factory
|
||||
[& {:keys [id schema]}]
|
||||
@@ -236,10 +236,8 @@
|
||||
(sm/check-fn schema)
|
||||
(constantly nil))]
|
||||
(fn [context]
|
||||
(assert (valid-context? context) "expected a valid context")
|
||||
(check-fn context)
|
||||
|
||||
(let [email (build-email-template id context)]
|
||||
(let [context (-> context check-context check-fn)
|
||||
email (build-email-template id context)]
|
||||
(when-not email
|
||||
(ex/raise :type :internal
|
||||
:code :email-template-does-not-exists
|
||||
@@ -271,7 +269,7 @@
|
||||
"Schedule an already defined email to be sent using asynchronously
|
||||
using worker task."
|
||||
[{:keys [::conn ::factory] :as context}]
|
||||
(assert (db/connection? conn) "expected a valid database connection")
|
||||
(assert (db/connectable? conn) "expected a valid database connection or pool")
|
||||
|
||||
(let [email (if factory
|
||||
(factory context)
|
||||
@@ -348,7 +346,7 @@
|
||||
[:subject ::sm/text]
|
||||
[:content ::sm/text]])
|
||||
|
||||
(def feedback
|
||||
(def user-feedback
|
||||
"A profile feedback email."
|
||||
(template-factory
|
||||
:id ::feedback
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
(declare ^:private send-feedback!)
|
||||
(declare ^:private send-user-feedback!)
|
||||
|
||||
(def ^:private schema:send-user-feedback
|
||||
[:map {:title "send-user-feedback"}
|
||||
@@ -34,14 +34,16 @@
|
||||
:hint "feedback not enabled"))
|
||||
|
||||
(let [profile (profile/get-profile pool profile-id)]
|
||||
(send-feedback! pool profile params)
|
||||
(send-user-feedback! pool profile params)
|
||||
nil))
|
||||
|
||||
(defn- send-feedback!
|
||||
(defn- send-user-feedback!
|
||||
[pool profile params]
|
||||
(let [dest (cf/get :feedback-destination)]
|
||||
(let [dest (or (cf/get :user-feedback-destination)
|
||||
;; LEGACY
|
||||
(cf/get :feedback-destination))]
|
||||
(eml/send! {::eml/conn pool
|
||||
::eml/factory eml/feedback
|
||||
::eml/factory eml/user-feedback
|
||||
:from dest
|
||||
:to dest
|
||||
:profile profile
|
||||
|
||||
@@ -118,11 +118,12 @@
|
||||
;; feature on frontend and make it permanent on file
|
||||
features (-> (:features params #{})
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union features))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features))]
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features)))]
|
||||
|
||||
(quotes/check! cfg {::quotes/id ::quotes/files-per-project
|
||||
::quotes/team-id team-id
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
" where file_id=? and tag=? and deleted_at is null")
|
||||
res (db/exec! conn [sql file-id tag])]
|
||||
(->> res
|
||||
(d/index-by :object-id (fn [row]
|
||||
(files/resolve-public-uri (:media-id row))))
|
||||
(d/index-by :object-id :media-id)
|
||||
(d/without-nils))))
|
||||
|
||||
(defn- get-object-thumbnails
|
||||
@@ -62,8 +61,7 @@
|
||||
" where file_id=? and deleted_at is null")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(->> res
|
||||
(d/index-by :object-id (fn [row]
|
||||
(files/resolve-public-uri (:media-id row))))
|
||||
(d/index-by :object-id :media-id)
|
||||
(d/without-nils))))
|
||||
|
||||
([conn file-id object-ids]
|
||||
@@ -75,8 +73,7 @@
|
||||
res (db/exec! conn [sql file-id ids])]
|
||||
|
||||
(->> res
|
||||
(d/index-by :object-id (fn [row]
|
||||
(files/resolve-public-uri (:media-id row))))
|
||||
(d/index-by :object-id :media-id)
|
||||
(d/without-nils)))))
|
||||
|
||||
(sv/defmethod ::get-file-object-thumbnails
|
||||
@@ -127,8 +124,11 @@
|
||||
(if-let [frame (-> frames first)]
|
||||
(let [frame-id (:id frame)
|
||||
object-id (thc/fmt-object-id (:id file) page-id frame-id "frame")
|
||||
frame (if-let [thumb (get thumbnails object-id)]
|
||||
(assoc frame :thumbnail thumb :shapes [])
|
||||
|
||||
frame (if-let [media-id (get thumbnails object-id)]
|
||||
(-> frame
|
||||
(assoc :thumbnail-id media-id)
|
||||
(assoc :shapes []))
|
||||
(dissoc frame :thumbnail))
|
||||
|
||||
children-ids
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features features)
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features))
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[app.worker :as wrk]))
|
||||
[app.worker :as wrk]
|
||||
[clojure.set :as set]))
|
||||
|
||||
;; --- Helpers & Specs
|
||||
|
||||
@@ -416,6 +417,7 @@
|
||||
::quotes/profile-id profile-id})
|
||||
|
||||
(let [features (-> (cfeat/get-enabled-features cf/flags)
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(cfeat/check-client-features! (:features params)))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
|
||||
@@ -532,17 +532,20 @@
|
||||
team-owner (get-team-owner conn team-id)
|
||||
|
||||
file (when (some? file-id)
|
||||
(get-file-for-team-access-request cfg file-id))
|
||||
(get-file-for-team-access-request cfg file-id))]
|
||||
|
||||
request (upsert-team-access-request conn team-id profile-id)]
|
||||
|
||||
;; FIXME missing quotes
|
||||
(-> cfg
|
||||
(assoc ::quotes/profile-id profile-id)
|
||||
(assoc ::quotes/team-id team-id)
|
||||
(quotes/check! {::quotes/id ::quotes/team-access-requests-per-team}
|
||||
{::quotes/id ::quotes/team-access-requests-per-requester}))
|
||||
|
||||
(teams/check-profile-muted conn requester)
|
||||
(teams/check-email-bounce conn (:email team-owner) false)
|
||||
(teams/check-email-spam conn (:email team-owner) true)
|
||||
|
||||
(let [factory (cond
|
||||
(let [request (upsert-team-access-request conn team-id profile-id)
|
||||
factory (cond
|
||||
(and (some? file) (:is-default team) is-viewer)
|
||||
eml/request-file-access-yourpenpot-view
|
||||
|
||||
@@ -565,9 +568,9 @@
|
||||
:team-id team-id
|
||||
:file-name (:name file)
|
||||
:file-id file-id
|
||||
:page-id (:page-id file)}))
|
||||
:page-id (:page-id file)})
|
||||
|
||||
(with-meta {:request request}
|
||||
{::audit/props {:request 1}})))
|
||||
(with-meta {:request request}
|
||||
{::audit/props {:request 1}}))))
|
||||
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: SNAPSHOTS-PER-FILE
|
||||
;; QUOTE: SNAPSHOTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:snapshots-per-team
|
||||
@@ -472,6 +472,57 @@
|
||||
(assoc ::count-sql [sql:get-snapshots-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAM-ACCESS-REQUESTS-PER-TEAM
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:team-access-requests-per-team
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]
|
||||
[::team-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-team-access-requests-per-team-quote?
|
||||
(sm/lazy-validator schema:team-access-requests-per-team))
|
||||
|
||||
(def ^:private sql:get-team-access-requests-per-team
|
||||
"SELECT count(*) AS total
|
||||
FROM team_access_request AS tar
|
||||
WHERE tar.team_id = ?")
|
||||
|
||||
(defmethod check-quote ::team-access-requests-per-team
|
||||
[{:keys [::profile-id ::team-id ::target] :as quote}]
|
||||
(assert (valid-team-access-requests-per-team-quote? quote) "invalid quote parameters")
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-team-access-requests-per-team Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-2 target team-id profile-id profile-id])
|
||||
(assoc ::count-sql [sql:get-team-access-requests-per-team team-id])
|
||||
(generic-check!)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: TEAM-ACCESS-REQUESTS-PER-REQUESTER
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:team-access-requests-per-requester
|
||||
[:map
|
||||
[::profile-id ::sm/uuid]])
|
||||
|
||||
(def ^:private valid-team-access-requests-per-requester-quote?
|
||||
(sm/lazy-validator schema:team-access-requests-per-requester))
|
||||
|
||||
(def ^:private sql:get-team-access-requests-per-requester
|
||||
"SELECT count(*) AS total
|
||||
FROM team_access_request AS tar
|
||||
WHERE tar.requester_id = ?")
|
||||
|
||||
(defmethod check-quote ::team-access-requests-per-requester
|
||||
[{:keys [::profile-id ::target] :as quote}]
|
||||
(assert (valid-team-access-requests-per-requester-quote? quote) "invalid quote parameters")
|
||||
(-> quote
|
||||
(assoc ::default (cf/get :quotes-team-access-requests-per-requester Integer/MAX_VALUE))
|
||||
(assoc ::quote-sql [sql:get-quotes-1 target profile-id])
|
||||
(assoc ::count-sql [sql:get-team-access-requests-per-requester profile-id])
|
||||
(generic-check!)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; QUOTE: DEFAULT
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -1090,8 +1090,7 @@
|
||||
(t/is (contains? result :file-id))
|
||||
|
||||
(t/is (= (:id file) (:file-id result)))
|
||||
(t/is (str/starts-with? (get-in result [:page :objects frame1-id :thumbnail])
|
||||
"http://localhost:3449/assets/by-id/"))
|
||||
(t/is (uuid? (get-in result [:page :objects frame1-id :thumbnail-id])))
|
||||
(t/is (= [] (get-in result [:page :objects frame1-id :shapes]))))
|
||||
|
||||
;; Delete thumbnail data
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
@@ -15,6 +15,8 @@
|
||||
"sax": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"shadow-cljs": "2.28.18",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.17.0"
|
||||
@@ -23,9 +25,9 @@
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"lint:clj": "clj-kondo --parallel=true --lint src/",
|
||||
"test:watch": "clojure -M:dev:shadow-cljs watch test",
|
||||
"test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'",
|
||||
"test:run": "node target/test.js",
|
||||
"test": "yarn run test:compile && yarn run test:run"
|
||||
"lint": "yarn run lint:clj",
|
||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "yarn run build:test && node target/tests/test.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
{:deps {:aliases [:dev]}
|
||||
:builds
|
||||
{:test
|
||||
{:target :node-test
|
||||
:output-to "target/test.js"
|
||||
:output-dir "target/test/"
|
||||
:ns-regexp "^common-tests.*-test$"
|
||||
:autorun true
|
||||
{:target :esm
|
||||
:output-dir "target/tests"
|
||||
:runtime :node
|
||||
:js-options {:js-provider :import}
|
||||
|
||||
:compiler-options
|
||||
{:output-feature-set :es-next
|
||||
:output-wrapper false
|
||||
:source-map true
|
||||
:source-map-include-sources-content true
|
||||
:source-map-detail-level :all
|
||||
:warnings {:fn-deprecated false}}}
|
||||
|
||||
:modules
|
||||
{:test {:init-fn common-tests.runner/-main
|
||||
:prepend-js "globalThis.navigator = {userAgent: \"\"}"}}}
|
||||
|
||||
:bench
|
||||
{:target :node-script
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
#{"fdata/shape-data-type"
|
||||
"styles/v2"
|
||||
"layout/grid"
|
||||
"components/v2"})
|
||||
"components/v2"
|
||||
"plugins/runtime"})
|
||||
|
||||
;; A set of features which only affects on frontend and can be enabled
|
||||
;; and disabled freely by the user any time. This features does not
|
||||
@@ -154,6 +155,7 @@
|
||||
team-features (into #{} xf-remove-ephimeral (:features team))]
|
||||
(-> enabled-features
|
||||
(set/intersection no-migration-features)
|
||||
(set/difference frontend-only-features)
|
||||
(set/union team-features))))
|
||||
|
||||
(defn check-client-features!
|
||||
|
||||
@@ -27,10 +27,22 @@
|
||||
#?(:clj (Instant/now)
|
||||
:cljs (.local ^js DateTime)))
|
||||
|
||||
#?(:clj
|
||||
(defn is-after?
|
||||
[one other]
|
||||
(.isAfter one other)))
|
||||
(defn is-after?
|
||||
"Analgous to: da > db"
|
||||
[da db]
|
||||
(let [result (compare da db)]
|
||||
(cond
|
||||
(neg? result) false
|
||||
(zero? result) false
|
||||
:else true)))
|
||||
|
||||
(defn is-before?
|
||||
[da db]
|
||||
(let [result (compare da db)]
|
||||
(cond
|
||||
(neg? result) true
|
||||
(zero? result) false
|
||||
:else false)))
|
||||
|
||||
(defn instant?
|
||||
[o]
|
||||
|
||||
@@ -529,13 +529,6 @@
|
||||
(or (d/not-empty? (dm/get-prop modifiers :geometry-child))
|
||||
(d/not-empty? (dm/get-prop modifiers :structure-child))))
|
||||
|
||||
(defn only-move?
|
||||
"Returns true if there are only move operations"
|
||||
[modifiers]
|
||||
(let [move-op? #(= :move (dm/get-prop % :type))]
|
||||
(and (every? move-op? (dm/get-prop modifiers :geometry-child))
|
||||
(every? move-op? (dm/get-prop modifiers :geometry-parent)))))
|
||||
|
||||
(defn has-geometry?
|
||||
[modifiers]
|
||||
(or (d/not-empty? (dm/get-prop modifiers :geometry-parent))
|
||||
@@ -550,6 +543,14 @@
|
||||
[modifiers]
|
||||
(d/not-empty? (dm/get-prop modifiers :structure-child)))
|
||||
|
||||
(defn only-move?
|
||||
"Returns true if there are only move operations"
|
||||
[modifiers]
|
||||
(let [move-op? #(= :move (dm/get-prop % :type))]
|
||||
(and (not (has-structure? modifiers))
|
||||
(every? move-op? (dm/get-prop modifiers :geometry-child))
|
||||
(every? move-op? (dm/get-prop modifiers :geometry-parent)))))
|
||||
|
||||
;; Extract subsets of modifiers
|
||||
|
||||
(defn select-child
|
||||
|
||||
89
common/test/common_tests/runner.cljc
Normal file
89
common/test/common_tests/runner.cljc
Normal file
@@ -0,0 +1,89 @@
|
||||
;; 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 common-tests.runner
|
||||
(:require
|
||||
[clojure.test :as t]
|
||||
[common-tests.colors-test]
|
||||
[common-tests.data-test]
|
||||
[common-tests.files-builder-test]
|
||||
[common-tests.files-changes-test]
|
||||
[common-tests.files-migrations-test]
|
||||
[common-tests.geom-point-test]
|
||||
[common-tests.geom-shapes-test]
|
||||
[common-tests.geom-test]
|
||||
[common-tests.logic.chained-propagation-test]
|
||||
[common-tests.logic.comp-creation-test]
|
||||
[common-tests.logic.comp-detach-with-nested-test]
|
||||
[common-tests.logic.comp-remove-swap-slots-test]
|
||||
[common-tests.logic.comp-reset-test]
|
||||
[common-tests.logic.comp-sync-test]
|
||||
[common-tests.logic.comp-touched-test]
|
||||
[common-tests.logic.copying-and-duplicating-test]
|
||||
[common-tests.logic.duplicated-pages-test]
|
||||
[common-tests.logic.move-shapes-test]
|
||||
[common-tests.logic.multiple-nesting-levels-test]
|
||||
[common-tests.logic.swap-and-reset-test]
|
||||
[common-tests.logic.swap-as-override-test]
|
||||
[common-tests.pages-helpers-test]
|
||||
[common-tests.record-test]
|
||||
[common-tests.schema-test]
|
||||
[common-tests.svg-path-test]
|
||||
[common-tests.svg-test]
|
||||
[common-tests.text-test]
|
||||
[common-tests.time-test]
|
||||
[common-tests.types-modifiers-test]
|
||||
[common-tests.types-shape-interactions-test]
|
||||
[common-tests.types.shape-decode-encode-test]
|
||||
[common-tests.types.tokens-lib-test]
|
||||
[common-tests.types.types-component-test]
|
||||
[common-tests.types.types-libraries-test]))
|
||||
|
||||
#?(:cljs (enable-console-print!))
|
||||
|
||||
#?(:cljs
|
||||
(defmethod cljs.test/report [:cljs.test/default :end-run-tests] [m]
|
||||
(if (cljs.test/successful? m)
|
||||
(.exit js/process 0)
|
||||
(.exit js/process 1))))
|
||||
|
||||
(defn -main
|
||||
[& args]
|
||||
(t/run-tests
|
||||
'common-tests.colors-test
|
||||
'common-tests.data-test
|
||||
'common-tests.files-builder-test
|
||||
'common-tests.files-changes-test
|
||||
'common-tests.files-migrations-test
|
||||
'common-tests.geom-point-test
|
||||
'common-tests.geom-shapes-test
|
||||
'common-tests.geom-test
|
||||
'common-tests.logic.chained-propagation-test
|
||||
'common-tests.logic.comp-creation-test
|
||||
'common-tests.logic.comp-detach-with-nested-test
|
||||
'common-tests.logic.comp-remove-swap-slots-test
|
||||
'common-tests.logic.comp-reset-test
|
||||
'common-tests.logic.comp-sync-test
|
||||
'common-tests.logic.comp-touched-test
|
||||
'common-tests.logic.copying-and-duplicating-test
|
||||
'common-tests.logic.duplicated-pages-test
|
||||
'common-tests.logic.move-shapes-test
|
||||
'common-tests.logic.multiple-nesting-levels-test
|
||||
'common-tests.logic.swap-and-reset-test
|
||||
'common-tests.logic.swap-as-override-test
|
||||
'common-tests.pages-helpers-test
|
||||
'common-tests.record-test
|
||||
'common-tests.schema-test
|
||||
'common-tests.svg-path-test
|
||||
'common-tests.svg-test
|
||||
'common-tests.text-test
|
||||
'common-tests.types-modifiers-test
|
||||
'common-tests.types-shape-interactions-test
|
||||
'common-tests.types.shape-decode-encode-test
|
||||
'common-tests.types.types-component-test
|
||||
'common-tests.types.types-libraries-test
|
||||
'common-tests.types.tokens-lib-test
|
||||
'common-tests.time-test))
|
||||
16
common/test/common_tests/time_test.cljc
Normal file
16
common/test/common_tests/time_test.cljc
Normal file
@@ -0,0 +1,16 @@
|
||||
;; 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 common-tests.time-test
|
||||
(:require
|
||||
[app.common.time :as dt]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest compare-time
|
||||
(let [dta (dt/parse-instant 10000)
|
||||
dtb (dt/parse-instant 20000)]
|
||||
(t/is (false? (dt/is-after? dta dtb)))
|
||||
(t/is (true? (dt/is-before? dta dtb)))))
|
||||
@@ -148,4 +148,4 @@
|
||||
;; (app.common.pprint/pprint shape)
|
||||
;; (app.common.pprint/pprint shape-3)
|
||||
(= shape shape-3)))
|
||||
{:num 1000})))
|
||||
{:num 100})))
|
||||
|
||||
@@ -14,8 +14,16 @@
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/testing "token"
|
||||
(t/deftest make-token
|
||||
(defn setup-virtual-time
|
||||
[next]
|
||||
(let [current (volatile! (inst-ms (dt/now)))]
|
||||
(with-redefs [dt/now #(dt/parse-instant (vswap! current inc))]
|
||||
(next))))
|
||||
|
||||
(t/use-fixtures :once setup-virtual-time)
|
||||
|
||||
(t/deftest tokens
|
||||
(t/testing "make-token"
|
||||
(let [now (dt/now)
|
||||
token1 (ctob/make-token :name "test-token-1"
|
||||
:type :boolean
|
||||
@@ -40,14 +48,14 @@
|
||||
(t/is (= (:modified-at token2) now))
|
||||
(t/is (ctob/valid-token? token2))))
|
||||
|
||||
(t/deftest invalid-tokens
|
||||
(t/testing "invalid-tokens"
|
||||
(let [args {:name 777
|
||||
:type :invalid}]
|
||||
(t/is (thrown-with-msg? Exception #"expected valid token"
|
||||
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid token"
|
||||
(apply ctob/make-token args)))
|
||||
(t/is (false? (ctob/valid-token? {})))))
|
||||
|
||||
(t/deftest find-token-value-references
|
||||
(t/testing "find-token-value-references"
|
||||
(t/testing "finds references inside curly braces in a string"
|
||||
(t/is (= #{"foo" "bar"} (ctob/find-token-value-references "{foo} + {bar}")))
|
||||
(t/testing "ignores extra text"
|
||||
@@ -57,8 +65,8 @@
|
||||
(t/testing "handles edge-case for extra curly braces"
|
||||
(t/is (= #{"foo" "bar"} (ctob/find-token-value-references "{foo}} + {bar}"))))))
|
||||
|
||||
(t/testing "token-set"
|
||||
(t/deftest make-token-set
|
||||
(t/deftest token-set
|
||||
(t/testing "make-token-set"
|
||||
(let [now (dt/now)
|
||||
token-set1 (ctob/make-token-set :name "test-token-set-1")
|
||||
token-set2 (ctob/make-token-set :name "test-token-set-2"
|
||||
@@ -76,13 +84,13 @@
|
||||
(t/is (= (:modified-at token-set2) now))
|
||||
(t/is (empty? (:tokens token-set2)))))
|
||||
|
||||
(t/deftest invalid-token-set
|
||||
(t/testing "invalid-token-set"
|
||||
(let [args {:name 777
|
||||
:description 999}]
|
||||
(t/is (thrown-with-msg? Exception #"expected valid token set"
|
||||
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid token set"
|
||||
(apply ctob/make-token-set args)))))
|
||||
|
||||
(t/deftest move-token-set
|
||||
(t/testing "move-token-set"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "A"))
|
||||
(ctob/add-set (ctob/make-token-set :name "B"))
|
||||
@@ -107,7 +115,7 @@
|
||||
(t/is (= original-order (move "A" "foo/bar/baz")))
|
||||
(t/is (= original-order (move "Missing" "Move"))))))
|
||||
|
||||
(t/deftest tokens-tree
|
||||
(t/testing "tokens-tree"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "A"
|
||||
:tokens {"foo.bar.baz" (ctob/make-token :name "foo.bar.baz"
|
||||
@@ -125,8 +133,8 @@
|
||||
(t/is (= (get-in expected ["foo" "bar" "bam" :name]) "foo.bar.bam"))
|
||||
(t/is (= (get-in expected ["baz" "boo" :name]) "baz.boo")))))
|
||||
|
||||
(t/testing "token-theme"
|
||||
(t/deftest make-token-theme
|
||||
(t/deftest token-theme
|
||||
(t/testing "make-token-theme"
|
||||
(let [now (dt/now)
|
||||
token-theme1 (ctob/make-token-theme :name "test-token-theme-1")
|
||||
token-theme2 (ctob/make-token-theme :name "test-token-theme-2"
|
||||
@@ -150,24 +158,24 @@
|
||||
(t/is (= (:modified-at token-theme2) now))
|
||||
(t/is (empty? (:sets token-theme2)))))
|
||||
|
||||
(t/deftest invalid-token-theme
|
||||
(t/testing "invalid-token-theme"
|
||||
(let [args {:name 777
|
||||
:group nil
|
||||
:description 999
|
||||
:is-source 42}]
|
||||
(t/is (thrown-with-msg? Exception #"expected valid token theme"
|
||||
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid token theme"
|
||||
(apply ctob/make-token-theme args))))))
|
||||
|
||||
|
||||
(t/testing "tokens-lib"
|
||||
(t/deftest make-tokens-lib
|
||||
(t/deftest tokens-lib
|
||||
(t/testing "make-tokens-lib"
|
||||
(let [tokens-lib (ctob/make-tokens-lib)]
|
||||
(t/is (= (ctob/set-count tokens-lib) 0))))
|
||||
|
||||
(t/deftest invalid-tokens-lib
|
||||
(t/testing "invalid-tokens-lib"
|
||||
(let [args {:sets nil
|
||||
:themes nil}]
|
||||
(t/is (thrown-with-msg? Exception #"expected valid tokens lib"
|
||||
(t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) #"expected valid tokens lib"
|
||||
(apply ctob/make-tokens-lib args))))))
|
||||
|
||||
|
||||
@@ -263,8 +271,8 @@
|
||||
(t/is (nil? token-set')))))
|
||||
|
||||
|
||||
(t/testing "token in a lib"
|
||||
(t/deftest add-token
|
||||
(t/deftest token-in-a-lib
|
||||
(t/testing "add-token"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set")))
|
||||
token (ctob/make-token :name "test-token"
|
||||
@@ -283,7 +291,7 @@
|
||||
(t/is (= (:name token') "test-token"))
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
|
||||
|
||||
(t/deftest update-token
|
||||
(t/testing "update-token"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -324,7 +332,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
|
||||
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
|
||||
|
||||
(t/deftest rename-token
|
||||
(t/testing "rename-token"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -356,7 +364,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
|
||||
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
|
||||
|
||||
(t/deftest delete-token
|
||||
(t/testing "delete-token"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -377,7 +385,7 @@
|
||||
(t/is (nil? token'))
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
|
||||
|
||||
(t/deftest list-active-themes-tokens-in-order
|
||||
(t/testing "list-active-themes-tokens-in-order"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :name "out-of-order-theme"
|
||||
;; Out of order sets in theme
|
||||
@@ -405,8 +413,8 @@
|
||||
(t/is (= ["set-a-token" "set-b-token"] expected-token-names)))))
|
||||
|
||||
|
||||
(t/testing "token-theme in a lib"
|
||||
(t/deftest add-token-theme
|
||||
(t/deftest token-theme-in-a-lib
|
||||
(t/testing "add-token-theme"
|
||||
(let [tokens-lib (ctob/make-tokens-lib)
|
||||
token-theme (ctob/make-token-theme :name "test-token-theme")
|
||||
tokens-lib' (ctob/add-theme tokens-lib token-theme)
|
||||
@@ -418,7 +426,7 @@
|
||||
(t/is (= (first token-themes') token-theme))
|
||||
(t/is (= token-theme' token-theme))))
|
||||
|
||||
(t/deftest update-token-theme
|
||||
(t/testing "update-token-theme"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-token-theme")))
|
||||
|
||||
@@ -440,7 +448,7 @@
|
||||
(t/is (= (:description token-theme') "some description"))
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme)))))
|
||||
|
||||
(t/deftest rename-token-theme
|
||||
(t/testing "rename-token-theme"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-token-theme")))
|
||||
|
||||
@@ -457,7 +465,7 @@
|
||||
(t/is (= (:name token-theme') "updated-name"))
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme)))))
|
||||
|
||||
(t/deftest delete-token-theme
|
||||
(t/testing "delete-token-theme"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-token-theme")))
|
||||
|
||||
@@ -470,7 +478,7 @@
|
||||
(t/is (= (ctob/theme-count tokens-lib') 0))
|
||||
(t/is (nil? token-theme'))))
|
||||
|
||||
(t/deftest toggle-set-in-theme
|
||||
(t/testing "toggle-set-in-theme"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-2"))
|
||||
@@ -487,8 +495,8 @@
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme))))))
|
||||
|
||||
|
||||
(t/testing "serialization"
|
||||
(t/deftest transit-serialization
|
||||
(t/deftest serialization
|
||||
(t/testing "transit-serialization"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set" (ctob/make-token :name "test-token"
|
||||
@@ -503,23 +511,24 @@
|
||||
(t/is (= (ctob/set-count tokens-lib') 1))
|
||||
(t/is (= (ctob/theme-count tokens-lib') 1))))
|
||||
|
||||
(t/deftest fressian-serialization
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set" (ctob/make-token :name "test-token"
|
||||
:type :boolean
|
||||
:value true))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-token-theme"))
|
||||
(ctob/toggle-set-in-theme "" "test-token-theme" "test-token-set"))
|
||||
encoded-blob (fres/encode tokens-lib)
|
||||
tokens-lib' (fres/decode encoded-blob)]
|
||||
#?(:clj
|
||||
(t/testing "fressian-serialization"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set" (ctob/make-token :name "test-token"
|
||||
:type :boolean
|
||||
:value true))
|
||||
(ctob/add-theme (ctob/make-token-theme :name "test-token-theme"))
|
||||
(ctob/toggle-set-in-theme "" "test-token-theme" "test-token-set"))
|
||||
encoded-blob (fres/encode tokens-lib)
|
||||
tokens-lib' (fres/decode encoded-blob)]
|
||||
|
||||
(t/is (ctob/valid-tokens-lib? tokens-lib'))
|
||||
(t/is (= (ctob/set-count tokens-lib') 1))
|
||||
(t/is (= (ctob/theme-count tokens-lib') 1)))))
|
||||
(t/is (ctob/valid-tokens-lib? tokens-lib'))
|
||||
(t/is (= (ctob/set-count tokens-lib') 1))
|
||||
(t/is (= (ctob/theme-count tokens-lib') 1))))))
|
||||
|
||||
(t/testing "grouping"
|
||||
(t/deftest split-and-join
|
||||
(t/deftest grouping
|
||||
(t/testing "split-and-join"
|
||||
(let [name "group/subgroup/name"
|
||||
path (ctob/split-path name "/")
|
||||
name' (ctob/join-path path "/")]
|
||||
@@ -528,14 +537,14 @@
|
||||
(t/is (= (nth path 2) "name"))
|
||||
(t/is (= name' name))))
|
||||
|
||||
(t/deftest remove-spaces
|
||||
(t/testing "remove-spaces"
|
||||
(let [name "group / subgroup / name"
|
||||
path (ctob/split-path name "/")]
|
||||
(t/is (= (first path) "group"))
|
||||
(t/is (= (second path) "subgroup"))
|
||||
(t/is (= (nth path 2) "name"))))
|
||||
|
||||
(t/deftest group-and-ungroup
|
||||
(t/testing "group-and-ungroup"
|
||||
(let [token-set1 (ctob/make-token-set :name "token-set1")
|
||||
token-set2 (ctob/make-token-set :name "some group/token-set2")
|
||||
|
||||
@@ -548,7 +557,7 @@
|
||||
(t/is (= (:name token-set1'') "token-set1"))
|
||||
(t/is (= (:name token-set2'') "some group/token-set2"))))
|
||||
|
||||
(t/deftest get-groups-str
|
||||
(t/testing "get-groups-str"
|
||||
(let [token-set1 (ctob/make-token-set :name "token-set1")
|
||||
token-set2 (ctob/make-token-set :name "some-group/token-set2")
|
||||
token-set3 (ctob/make-token-set :name "some-group/some-subgroup/token-set3")]
|
||||
@@ -556,7 +565,7 @@
|
||||
(t/is (= (ctob/get-groups-str token-set2 "/") "some-group"))
|
||||
(t/is (= (ctob/get-groups-str token-set3 "/") "some-group/some-subgroup"))))
|
||||
|
||||
(t/deftest get-final-name
|
||||
(t/testing "get-final-name"
|
||||
(let [token-set1 (ctob/make-token-set :name "token-set1")
|
||||
token-set2 (ctob/make-token-set :name "some-group/token-set2")
|
||||
token-set3 (ctob/make-token-set :name "some-group/some-subgroup/token-set3")]
|
||||
@@ -565,7 +574,7 @@
|
||||
(t/is (= (ctob/get-final-name token-set3 "/") "token-set3"))))
|
||||
|
||||
(t/testing "grouped tokens"
|
||||
(t/deftest grouped-tokens
|
||||
(t/testing "grouped-tokens"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -599,7 +608,7 @@
|
||||
(t/is (= (:name (nth tokens-list 3)) "group1.subgroup11.token4"))
|
||||
(t/is (= (:name (nth tokens-list 4)) "group2.token5"))))
|
||||
|
||||
(t/deftest update-token-in-groups
|
||||
(t/testing "update-token-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -634,7 +643,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
|
||||
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
|
||||
|
||||
(t/deftest rename-token-in-groups
|
||||
(t/testing "rename-token-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -668,7 +677,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
|
||||
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
|
||||
|
||||
(t/deftest move-token-of-group
|
||||
(t/testing "move-token-of-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -703,7 +712,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))
|
||||
(t/is (dt/is-after? (:modified-at token') (:modified-at token)))))
|
||||
|
||||
(t/deftest delete-token-in-group
|
||||
(t/testing "delete-token-in-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
|
||||
(ctob/add-token-in-set "test-token-set"
|
||||
@@ -727,7 +736,7 @@
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set))))))
|
||||
|
||||
(t/testing "grouped sets"
|
||||
(t/deftest grouped-sets
|
||||
(t/testing "grouped-sets"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "group1/token-set-2"))
|
||||
@@ -786,7 +795,7 @@
|
||||
(t/is (= (ctob/group? (second node-set5)) false))
|
||||
(t/is (= (:name (second node-set5)) "group2/token-set-5"))))
|
||||
|
||||
(t/deftest update-set-in-groups
|
||||
(t/testing "update-set-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "group1/token-set-2"))
|
||||
@@ -812,7 +821,7 @@
|
||||
(t/is (= (:description token-set') "some description"))
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
|
||||
|
||||
(t/deftest rename-set-in-groups
|
||||
(t/testing "rename-set-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "group1/token-set-2"))
|
||||
@@ -839,7 +848,7 @@
|
||||
(t/is (= (:description token-set') nil))
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
|
||||
|
||||
(t/deftest move-set-of-group
|
||||
(t/testing "move-set-of-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "group1/token-set-2"))
|
||||
@@ -868,7 +877,7 @@
|
||||
(t/is (= (:description token-set') nil))
|
||||
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
|
||||
|
||||
(t/deftest delete-set-in-group
|
||||
(t/testing "delete-set-in-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "token-set-1"))
|
||||
(ctob/add-set (ctob/make-token-set :name "group1/token-set-2")))
|
||||
@@ -884,7 +893,7 @@
|
||||
(t/is (nil? token-set')))))
|
||||
|
||||
(t/testing "grouped themes"
|
||||
(t/deftest grouped-themes
|
||||
(t/testing "grouped-themes"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2"))
|
||||
@@ -941,7 +950,7 @@
|
||||
(t/is (= (ctob/group? (second node-theme4)) false))
|
||||
(t/is (= (:name (second node-theme4)) "token-theme-4"))))
|
||||
|
||||
(t/deftest update-theme-in-groups
|
||||
(t/testing "update-theme-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2"))
|
||||
@@ -967,7 +976,7 @@
|
||||
(t/is (= (:description token-theme') "some description"))
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme)))))
|
||||
|
||||
(t/deftest get-theme-groups
|
||||
(t/testing "get-theme-groups"
|
||||
(let [token-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2"))
|
||||
@@ -976,13 +985,14 @@
|
||||
token-groups (ctob/get-theme-groups token-lib)]
|
||||
(t/is (= token-groups ["group1" "group2"]))))
|
||||
|
||||
(t/deftest rename-theme-in-groups
|
||||
(t/testing "rename-theme-in-groups"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-3"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group2" :name "token-theme-4")))
|
||||
|
||||
|
||||
tokens-lib' (-> tokens-lib
|
||||
(ctob/update-theme "group1" "token-theme-2"
|
||||
(fn [token-theme]
|
||||
@@ -1003,7 +1013,7 @@
|
||||
(t/is (= (:description token-theme') nil))
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme)))))
|
||||
|
||||
(t/deftest move-theme-of-group
|
||||
(t/testing "move-theme-of-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2"))
|
||||
@@ -1033,7 +1043,7 @@
|
||||
(t/is (= (:description token-theme') nil))
|
||||
(t/is (dt/is-after? (:modified-at token-theme') (:modified-at token-theme)))))
|
||||
|
||||
(t/deftest delete-theme-in-group
|
||||
(t/testing "delete-theme-in-group"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-theme (ctob/make-token-theme :group "" :name "token-theme-1"))
|
||||
(ctob/add-theme (ctob/make-token-theme :group "group1" :name "token-theme-2")))
|
||||
@@ -1049,8 +1059,8 @@
|
||||
(t/is (nil? token-theme'))))))
|
||||
|
||||
#?(:clj
|
||||
(t/testing "dtcg encoding/decoding"
|
||||
(t/deftest decode-dtcg-json
|
||||
(t/deftest dtcg-encoding-decoding
|
||||
(t/testing "decode-dtcg-json"
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-example.json")
|
||||
(tr/decode-str))
|
||||
lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json)
|
||||
@@ -1078,7 +1088,7 @@
|
||||
(t/testing "invalid tokens got discarded"
|
||||
(t/is (nil? (get-set-token "typography" "H1.Bold"))))))
|
||||
|
||||
(t/deftest encode-dtcg-json
|
||||
(t/testing "encode-dtcg-json"
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "core"
|
||||
:tokens {"colors.red.600"
|
||||
@@ -1111,7 +1121,7 @@
|
||||
"$type" "color"}}}}}
|
||||
expected))))
|
||||
|
||||
(t/deftest encode-decode-dtcg-json
|
||||
(t/testing "encode-decode-dtcg-json"
|
||||
(with-redefs [dt/now (constantly #inst "2024-10-16T12:01:20.257840055-00:00")]
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :name "core"
|
||||
|
||||
446
common/yarn.lock
446
common/yarn.lock
@@ -88,7 +88,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ansi-styles@npm:^4.0.0":
|
||||
"ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0":
|
||||
version: 4.3.0
|
||||
resolution: "ansi-styles@npm:4.3.0"
|
||||
dependencies:
|
||||
@@ -104,6 +104,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"anymatch@npm:~3.1.2":
|
||||
version: 3.1.3
|
||||
resolution: "anymatch@npm:3.1.3"
|
||||
dependencies:
|
||||
normalize-path: "npm:^3.0.0"
|
||||
picomatch: "npm:^2.0.4"
|
||||
checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asn1.js@npm:^4.10.1":
|
||||
version: 4.10.1
|
||||
resolution: "asn1.js@npm:4.10.1"
|
||||
@@ -139,6 +149,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"binary-extensions@npm:^2.0.0":
|
||||
version: 2.3.0
|
||||
resolution: "binary-extensions@npm:2.3.0"
|
||||
checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9":
|
||||
version: 4.12.0
|
||||
resolution: "bn.js@npm:4.12.0"
|
||||
@@ -153,6 +170,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
dependencies:
|
||||
balanced-match: "npm:^1.0.0"
|
||||
concat-map: "npm:0.0.1"
|
||||
checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "brace-expansion@npm:2.0.1"
|
||||
@@ -162,6 +189,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"braces@npm:~3.0.2":
|
||||
version: 3.0.3
|
||||
resolution: "braces@npm:3.0.3"
|
||||
dependencies:
|
||||
fill-range: "npm:^7.1.1"
|
||||
checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brorand@npm:^1.0.1, brorand@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "brorand@npm:1.1.0"
|
||||
@@ -308,6 +344,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chalk@npm:^4.1.2":
|
||||
version: 4.1.2
|
||||
resolution: "chalk@npm:4.1.2"
|
||||
dependencies:
|
||||
ansi-styles: "npm:^4.1.0"
|
||||
supports-color: "npm:^7.1.0"
|
||||
checksum: 10c0/4a3fef5cc34975c898ffe77141450f679721df9dde00f6c304353fa9c8b571929123b26a0e4617bde5018977eb655b31970c297b91b63ee83bb82aeb04666880
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:^3.5.2":
|
||||
version: 3.6.0
|
||||
resolution: "chokidar@npm:3.6.0"
|
||||
dependencies:
|
||||
anymatch: "npm:~3.1.2"
|
||||
braces: "npm:~3.0.2"
|
||||
fsevents: "npm:~2.3.2"
|
||||
glob-parent: "npm:~5.1.2"
|
||||
is-binary-path: "npm:~2.1.0"
|
||||
is-glob: "npm:~4.0.1"
|
||||
normalize-path: "npm:~3.0.0"
|
||||
readdirp: "npm:~3.6.0"
|
||||
dependenciesMeta:
|
||||
fsevents:
|
||||
optional: true
|
||||
checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "chownr@npm:2.0.0"
|
||||
@@ -332,6 +397,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cliui@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "cliui@npm:8.0.1"
|
||||
dependencies:
|
||||
string-width: "npm:^4.2.0"
|
||||
strip-ansi: "npm:^6.0.1"
|
||||
wrap-ansi: "npm:^7.0.0"
|
||||
checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-convert@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "color-convert@npm:2.0.1"
|
||||
@@ -352,14 +428,41 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "common@workspace:."
|
||||
dependencies:
|
||||
concurrently: "npm:^9.0.1"
|
||||
luxon: "npm:^3.4.4"
|
||||
nodemon: "npm:^3.1.7"
|
||||
sax: "npm:^1.4.1"
|
||||
shadow-cljs: "npm:2.28.11"
|
||||
shadow-cljs: "npm:2.28.18"
|
||||
source-map-support: "npm:^0.5.21"
|
||||
ws: "npm:^8.17.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"concat-map@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "concat-map@npm:0.0.1"
|
||||
checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concurrently@npm:^9.0.1":
|
||||
version: 9.1.0
|
||||
resolution: "concurrently@npm:9.1.0"
|
||||
dependencies:
|
||||
chalk: "npm:^4.1.2"
|
||||
lodash: "npm:^4.17.21"
|
||||
rxjs: "npm:^7.8.1"
|
||||
shell-quote: "npm:^1.8.1"
|
||||
supports-color: "npm:^8.1.1"
|
||||
tree-kill: "npm:^1.2.2"
|
||||
yargs: "npm:^17.7.2"
|
||||
bin:
|
||||
conc: dist/bin/concurrently.js
|
||||
concurrently: dist/bin/concurrently.js
|
||||
checksum: 10c0/f2f42f94dde508bfbaf47b5ac654db9e8a4bf07d3d7b6267dd058ae6f362eec677ae7c8ede398d081e5fd0d1de5811dc9a53e57d3f1f68e72ac6459db9e0896b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"console-browserify@npm:^1.1.0":
|
||||
version: 1.2.0
|
||||
resolution: "console-browserify@npm:1.2.0"
|
||||
@@ -460,6 +563,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:^4":
|
||||
version: 4.3.7
|
||||
resolution: "debug@npm:4.3.7"
|
||||
dependencies:
|
||||
ms: "npm:^2.1.3"
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "define-data-property@npm:1.1.4"
|
||||
@@ -585,6 +700,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"escalade@npm:^3.1.1":
|
||||
version: 3.2.0
|
||||
resolution: "escalade@npm:3.2.0"
|
||||
checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"events@npm:^3.0.0":
|
||||
version: 3.3.0
|
||||
resolution: "events@npm:3.3.0"
|
||||
@@ -610,6 +732,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fill-range@npm:^7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "fill-range@npm:7.1.1"
|
||||
dependencies:
|
||||
to-regex-range: "npm:^5.0.1"
|
||||
checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"foreground-child@npm:^3.1.0":
|
||||
version: 3.1.1
|
||||
resolution: "foreground-child@npm:3.1.1"
|
||||
@@ -638,6 +769,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@npm:~2.3.2":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@npm:2.3.3"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
|
||||
version: 2.3.3
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"function-bind@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "function-bind@npm:1.1.2"
|
||||
@@ -645,6 +795,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-caller-file@npm:^2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "get-caller-file@npm:2.0.5"
|
||||
checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "get-intrinsic@npm:1.2.4"
|
||||
@@ -658,6 +815,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob-parent@npm:~5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
dependencies:
|
||||
is-glob: "npm:^4.0.1"
|
||||
checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob@npm:^10.2.2, glob@npm:^10.3.10":
|
||||
version: 10.3.16
|
||||
resolution: "glob@npm:10.3.16"
|
||||
@@ -689,6 +855,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-flag@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "has-flag@npm:3.0.0"
|
||||
checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-flag@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "has-flag@npm:4.0.0"
|
||||
checksum: 10c0/2e789c61b7888d66993e14e8331449e525ef42aac53c627cc53d1c3334e768bcb6abdc4f5f0de1478a25beec6f0bd62c7549058b7ac53e924040d4f301f02fd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "has-property-descriptors@npm:1.0.2"
|
||||
@@ -813,6 +993,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore-by-default@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "ignore-by-default@npm:1.0.1"
|
||||
checksum: 10c0/9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"imurmurhash@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "imurmurhash@npm:0.1.4"
|
||||
@@ -851,6 +1038,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-binary-path@npm:~2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "is-binary-path@npm:2.1.0"
|
||||
dependencies:
|
||||
binary-extensions: "npm:^2.0.0"
|
||||
checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-extglob@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "is-extglob@npm:2.1.1"
|
||||
checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-fullwidth-code-point@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "is-fullwidth-code-point@npm:3.0.0"
|
||||
@@ -858,6 +1061,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1":
|
||||
version: 4.0.3
|
||||
resolution: "is-glob@npm:4.0.3"
|
||||
dependencies:
|
||||
is-extglob: "npm:^2.1.1"
|
||||
checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-lambda@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "is-lambda@npm:1.0.1"
|
||||
@@ -865,6 +1077,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-number@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "is-number@npm:7.0.0"
|
||||
checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isarray@npm:^1.0.0, isarray@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "isarray@npm:1.0.0"
|
||||
@@ -906,6 +1125,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash@npm:^4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
|
||||
version: 10.2.2
|
||||
resolution: "lru-cache@npm:10.2.2"
|
||||
@@ -977,6 +1203,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
dependencies:
|
||||
brace-expansion: "npm:^1.1.7"
|
||||
checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^9.0.1":
|
||||
version: 9.0.4
|
||||
resolution: "minimatch@npm:9.0.4"
|
||||
@@ -1086,6 +1321,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ms@npm:^2.1.3":
|
||||
version: 2.1.3
|
||||
resolution: "ms@npm:2.1.3"
|
||||
checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"negotiator@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "negotiator@npm:0.6.3"
|
||||
@@ -1144,6 +1386,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nodemon@npm:^3.1.7":
|
||||
version: 3.1.7
|
||||
resolution: "nodemon@npm:3.1.7"
|
||||
dependencies:
|
||||
chokidar: "npm:^3.5.2"
|
||||
debug: "npm:^4"
|
||||
ignore-by-default: "npm:^1.0.1"
|
||||
minimatch: "npm:^3.1.2"
|
||||
pstree.remy: "npm:^1.1.8"
|
||||
semver: "npm:^7.5.3"
|
||||
simple-update-notifier: "npm:^2.0.0"
|
||||
supports-color: "npm:^5.5.0"
|
||||
touch: "npm:^3.1.0"
|
||||
undefsafe: "npm:^2.0.5"
|
||||
bin:
|
||||
nodemon: bin/nodemon.js
|
||||
checksum: 10c0/e0b46939abdbce251b1d6281005a5763cee57db295bb00bc4a753b0f5320dac00fe53547fb6764c70a086cf6d1238875cccb800fbc71544b3ecbd3ef71183c87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^7.0.0":
|
||||
version: 7.2.1
|
||||
resolution: "nopt@npm:7.2.1"
|
||||
@@ -1155,6 +1417,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "normalize-path@npm:3.0.0"
|
||||
checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"object-inspect@npm:^1.13.1":
|
||||
version: 1.13.1
|
||||
resolution: "object-inspect@npm:1.13.1"
|
||||
@@ -1255,6 +1524,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1":
|
||||
version: 2.3.1
|
||||
resolution: "picomatch@npm:2.3.1"
|
||||
checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proc-log@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "proc-log@npm:3.0.0"
|
||||
@@ -1293,6 +1569,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pstree.remy@npm:^1.1.8":
|
||||
version: 1.1.8
|
||||
resolution: "pstree.remy@npm:1.1.8"
|
||||
checksum: 10c0/30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"public-encrypt@npm:^4.0.0":
|
||||
version: 4.0.3
|
||||
resolution: "public-encrypt@npm:4.0.3"
|
||||
@@ -1375,6 +1658,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readdirp@npm:~3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "readdirp@npm:3.6.0"
|
||||
dependencies:
|
||||
picomatch: "npm:^2.2.1"
|
||||
checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readline-sync@npm:^1.4.7":
|
||||
version: 1.4.10
|
||||
resolution: "readline-sync@npm:1.4.10"
|
||||
@@ -1382,6 +1674,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"require-directory@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "require-directory@npm:2.1.1"
|
||||
checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"retry@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "retry@npm:0.12.0"
|
||||
@@ -1399,6 +1698,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rxjs@npm:^7.8.1":
|
||||
version: 7.8.1
|
||||
resolution: "rxjs@npm:7.8.1"
|
||||
dependencies:
|
||||
tslib: "npm:^2.1.0"
|
||||
checksum: 10c0/3c49c1ecd66170b175c9cacf5cef67f8914dcbc7cd0162855538d365c83fea631167cacb644b3ce533b2ea0e9a4d0b12175186985f89d75abe73dbd8f7f06f68
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
@@ -1436,6 +1744,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"semver@npm:^7.5.3":
|
||||
version: 7.6.3
|
||||
resolution: "semver@npm:7.6.3"
|
||||
bin:
|
||||
semver: bin/semver.js
|
||||
checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"set-function-length@npm:^1.2.1":
|
||||
version: 1.2.2
|
||||
resolution: "set-function-length@npm:1.2.2"
|
||||
@@ -1476,9 +1793,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shadow-cljs@npm:2.28.11":
|
||||
version: 2.28.11
|
||||
resolution: "shadow-cljs@npm:2.28.11"
|
||||
"shadow-cljs@npm:2.28.18":
|
||||
version: 2.28.18
|
||||
resolution: "shadow-cljs@npm:2.28.18"
|
||||
dependencies:
|
||||
node-libs-browser: "npm:^2.2.1"
|
||||
readline-sync: "npm:^1.4.7"
|
||||
@@ -1488,7 +1805,7 @@ __metadata:
|
||||
ws: "npm:^7.4.6"
|
||||
bin:
|
||||
shadow-cljs: cli/runner.js
|
||||
checksum: 10c0/c5c77d524ee8f44e4ae2ddc196af170d02405cc8731ea71f852c7b220fc1ba8aaf5cf33753fd8a7566c8749bb75d360f903dfb0d131bcdc6c2c33f44404bd6a3
|
||||
checksum: 10c0/4732cd11a5722644a0a91ae5560a55f62432ae5317bd2d1fd5bf12af8354c81776f4fcfce5c477b43af1ac2ecd4a216887337e1b92cca37a1b8cb9c157a393c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1508,6 +1825,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"shell-quote@npm:^1.8.1":
|
||||
version: 1.8.1
|
||||
resolution: "shell-quote@npm:1.8.1"
|
||||
checksum: 10c0/8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"side-channel@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "side-channel@npm:1.0.6"
|
||||
@@ -1527,6 +1851,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-update-notifier@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "simple-update-notifier@npm:2.0.0"
|
||||
dependencies:
|
||||
semver: "npm:^7.5.3"
|
||||
checksum: 10c0/2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"smart-buffer@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "smart-buffer@npm:4.2.0"
|
||||
@@ -1627,7 +1960,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "string-width@npm:4.2.3"
|
||||
dependencies:
|
||||
@@ -1685,6 +2018,33 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "supports-color@npm:5.5.0"
|
||||
dependencies:
|
||||
has-flag: "npm:^3.0.0"
|
||||
checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^7.1.0":
|
||||
version: 7.2.0
|
||||
resolution: "supports-color@npm:7.2.0"
|
||||
dependencies:
|
||||
has-flag: "npm:^4.0.0"
|
||||
checksum: 10c0/afb4c88521b8b136b5f5f95160c98dee7243dc79d5432db7efc27efb219385bbc7d9427398e43dd6cc730a0f87d5085ce1652af7efbe391327bc0a7d0f7fc124
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"supports-color@npm:^8.1.1":
|
||||
version: 8.1.1
|
||||
resolution: "supports-color@npm:8.1.1"
|
||||
dependencies:
|
||||
has-flag: "npm:^4.0.0"
|
||||
checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||
version: 6.2.1
|
||||
resolution: "tar@npm:6.2.1"
|
||||
@@ -1715,6 +2075,40 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-regex-range@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "to-regex-range@npm:5.0.1"
|
||||
dependencies:
|
||||
is-number: "npm:^7.0.0"
|
||||
checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"touch@npm:^3.1.0":
|
||||
version: 3.1.1
|
||||
resolution: "touch@npm:3.1.1"
|
||||
bin:
|
||||
nodetouch: bin/nodetouch.js
|
||||
checksum: 10c0/d2e4d269a42c846a22a29065b9af0b263de58effc85a1764bb7a2e8fc4b47700e9e2fcbd7eb1f5bffbb7c73d860f93600cef282b93ddac8f0b62321cb498b36e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tree-kill@npm:^1.2.2":
|
||||
version: 1.2.2
|
||||
resolution: "tree-kill@npm:1.2.2"
|
||||
bin:
|
||||
tree-kill: cli.js
|
||||
checksum: 10c0/7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.1.0":
|
||||
version: 2.8.1
|
||||
resolution: "tslib@npm:2.8.1"
|
||||
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tty-browserify@npm:0.0.0":
|
||||
version: 0.0.0
|
||||
resolution: "tty-browserify@npm:0.0.0"
|
||||
@@ -1722,6 +2116,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undefsafe@npm:^2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "undefsafe@npm:2.0.5"
|
||||
checksum: 10c0/96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "unique-filename@npm:3.0.0"
|
||||
@@ -1815,7 +2216,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "wrap-ansi@npm:7.0.0"
|
||||
dependencies:
|
||||
@@ -1874,9 +2275,38 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"y18n@npm:^5.0.5":
|
||||
version: 5.0.8
|
||||
resolution: "y18n@npm:5.0.8"
|
||||
checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "yallist@npm:4.0.0"
|
||||
checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs-parser@npm:^21.1.1":
|
||||
version: 21.1.1
|
||||
resolution: "yargs-parser@npm:21.1.1"
|
||||
checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yargs@npm:^17.7.2":
|
||||
version: 17.7.2
|
||||
resolution: "yargs@npm:17.7.2"
|
||||
dependencies:
|
||||
cliui: "npm:^8.0.1"
|
||||
escalade: "npm:^3.1.1"
|
||||
get-caller-file: "npm:^2.0.5"
|
||||
require-directory: "npm:^2.1.1"
|
||||
string-width: "npm:^4.2.3"
|
||||
y18n: "npm:^5.0.5"
|
||||
yargs-parser: "npm:^21.1.1"
|
||||
checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -65,7 +65,7 @@ You will be able to share your plugin with the <a target="_blank" href="https://
|
||||
|
||||
### My plugin works on my local machine, but I couldn’t install it on Penpot. What could be the problem?
|
||||
|
||||
The url you that you need to provide in the plugin manager should look <a target="_blank" href="/plugins/create-a-plugin/#2.6.-step-6.-configure-the-manifest-file">like this</a>: <code class="language-bash">https:\/\/yourdomain.com/assents/manifest.json</code>
|
||||
The url you that you need to provide in the plugin manager should look <a target="_blank" href="/plugins/create-a-plugin/#2.6.-step-6.-configure-the-manifest-file">like this</a>: <code class="language-bash">https:\/\/yourdomain.com/assets/manifest.json</code>
|
||||
|
||||
### Where can I get support if I find a bug or an unexpected behavior?
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "yarn run build:test && node target/tests/test.js",
|
||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -w target/tests/test.js --exec 'sleep 2 && node target/tests/test.js'\"",
|
||||
"watch:test": "mkdir -p target/tests && concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests --exec 'node target/tests/test.js'\"",
|
||||
"test:e2e": "playwright test --project default",
|
||||
"translations": "node ./scripts/translations.js",
|
||||
"watch:app:assets": "node ./scripts/watch.js",
|
||||
@@ -100,7 +100,7 @@
|
||||
"@penpot/hljs": "portal:./vendor/hljs",
|
||||
"@penpot/mousetrap": "portal:./vendor/mousetrap",
|
||||
"@penpot/svgo": "penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b",
|
||||
"@penpot/text-editor": "penpot/penpot-text-editor#a100aad8d0efcbb070bed9144dbd2782547e78ba",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@tokens-studio/sd-transforms": "^0.16.1",
|
||||
"compression": "^1.7.4",
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
@@ -58,10 +58,11 @@ test("Save and restore version", async ({ page }) => {
|
||||
await page.getByRole("textbox").press("Enter");
|
||||
|
||||
await page
|
||||
.locator("li")
|
||||
.filter({ hasText: "INIT" })
|
||||
.getByRole("button")
|
||||
.click();
|
||||
.getByLabel("History", { exact: true })
|
||||
.locator("div")
|
||||
.nth(3)
|
||||
.hover();
|
||||
await page.getByRole("button", { name: "Open version menu" }).click();
|
||||
await page.getByRole("button", { name: "Restore" }).click();
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
|
||||
3
frontend/resources/images/icons/board-2.svg
Normal file
3
frontend/resources/images/icons/board-2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3.5 3.5h-2m2 0v-2m0 2h9m-9 0v9m9-9v-2m0 2h2m-2 0v9m0 0h2m-2 0v2m0-2h-9m0 0v2m0-2h-2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 214 B |
@@ -237,13 +237,18 @@ async function renderTemplate(path, context = {}, partials = {}) {
|
||||
return mustache.render(content, context, partials);
|
||||
}
|
||||
|
||||
const renderer = {
|
||||
link(href, title, text) {
|
||||
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||
const extension = {
|
||||
useNewRenderer: true,
|
||||
renderer: {
|
||||
link(token) {
|
||||
const href = token.href;
|
||||
const text = token.text;
|
||||
return `<a href="${href}" target="_blank">${text}</a>`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
marked.use({ renderer });
|
||||
marked.use(extension);
|
||||
|
||||
async function readTranslations() {
|
||||
const langs = [
|
||||
|
||||
@@ -90,7 +90,6 @@
|
||||
"unknown"
|
||||
date)))
|
||||
|
||||
|
||||
;; --- Global Config Vars
|
||||
|
||||
(def default-theme "default")
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
[app.util.i18n :as i18n]
|
||||
[app.util.theme :as theme]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[debug]
|
||||
[features]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -38,11 +39,11 @@
|
||||
(log/setup! {:app :info})
|
||||
|
||||
(when (= :browser cf/target)
|
||||
(log/info :message "Welcome to penpot"
|
||||
:version (:full cf/version)
|
||||
(log/info :version (:full cf/version)
|
||||
:asserts *assert*
|
||||
:build-date cf/build-date
|
||||
:public-uri (dm/str cf/public-uri)))
|
||||
:public-uri (dm/str cf/public-uri))
|
||||
(log/info :flags (str/join "," (map name cf/flags))))
|
||||
|
||||
(declare reinit)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.team :as ctt]
|
||||
[app.config :as cf]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.features :as features]
|
||||
@@ -75,15 +74,13 @@
|
||||
(watch [_ _ _]
|
||||
(case code
|
||||
:upgrade-version
|
||||
(when (or (not= (:version params) (:full cf/version))
|
||||
(true? (:force params)))
|
||||
(rx/of (ntf/dialog
|
||||
:content (tr "notifications.by-code.upgrade-version")
|
||||
:controls :inline-actions
|
||||
:type :inline
|
||||
:level level
|
||||
:actions [{:label "Refresh" :callback force-reload!}]
|
||||
:tag :notification)))
|
||||
(rx/of (ntf/dialog
|
||||
:content (tr "notifications.by-code.upgrade-version")
|
||||
:controls :inline-actions
|
||||
:type :inline
|
||||
:level level
|
||||
:actions [{:label "Refresh" :callback force-reload!}]
|
||||
:tag :notification))
|
||||
|
||||
:maintenance
|
||||
(rx/of (ntf/dialog
|
||||
|
||||
@@ -450,7 +450,9 @@
|
||||
(ptk/reify ::update-team
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:teams id :name] name))
|
||||
(-> state
|
||||
(assoc-in [:teams id :name] name)
|
||||
(assoc-in [:team :name] name)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
@@ -1118,6 +1120,9 @@
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
([team-id]
|
||||
(ptk/reify ::go-to-projects-1
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :current-team-id team-id))
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (rt/nav :dashboard-projects {:team-id team-id}))))))
|
||||
|
||||
@@ -199,18 +199,21 @@
|
||||
|
||||
;; Load libraries
|
||||
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
||||
(rx/mapcat identity)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at)))))
|
||||
(rx/merge-map fpmap/resolve-file)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id] :as file}]
|
||||
(->> (rp/cmd! :get-file-object-thumbnails {:file-id id :tag "component"})
|
||||
(rx/map #(assoc file :thumbnails %)))))
|
||||
(rx/reduce conj [])
|
||||
(rx/map libraries-fetched)))
|
||||
(rx/mapcat (fn [libraries]
|
||||
(rx/merge
|
||||
(->> (rx/from libraries)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at)))))
|
||||
(rx/merge-map fpmap/resolve-file)
|
||||
(rx/reduce conj [])
|
||||
(rx/map libraries-fetched))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
||||
(rx/map dwl/library-thumbnails-fetched)))))))
|
||||
|
||||
(rx/of (with-meta (workspace-initialized)
|
||||
{:file-id file-id})))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcm]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.common :as dwco]
|
||||
[app.main.data.workspace.drawing :as dwd]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
@@ -60,7 +61,11 @@
|
||||
(let [local (:comments-local state)]
|
||||
(cond
|
||||
(:draft local) (rx/of (dcm/close-thread))
|
||||
(:open local) (rx/of (dcm/close-thread)))))))
|
||||
(:open local) (rx/of (dcm/close-thread))
|
||||
|
||||
:else
|
||||
(rx/of (dw/clear-edition-mode)
|
||||
(dw/deselect-all true)))))))
|
||||
|
||||
;; Event responsible of the what should be executed when user clicked
|
||||
;; on the comments layer. An option can be create a new draft thread,
|
||||
|
||||
@@ -881,11 +881,9 @@
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(update-component shape-id undo-group)
|
||||
(sync-file current-file-id file-id :components (:component-id shape) undo-group)
|
||||
(update-component-thumbnail-sync state component-id file-id "frame")
|
||||
(update-component-thumbnail-sync state component-id file-id "component")
|
||||
(sync-file current-file-id file-id :components component-id undo-group)
|
||||
(when (not current-file?)
|
||||
(sync-file file-id file-id :components (:component-id shape) undo-group))
|
||||
(sync-file file-id file-id :components component-id undo-group))
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn launch-component-sync
|
||||
@@ -937,9 +935,9 @@
|
||||
;; in the grid creating new rows/columns to make space
|
||||
(let [file (wsh/get-file state file-id)
|
||||
libraries (wsh/get-libraries state)
|
||||
page (wsh/lookup-page state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
parent (get objects (:parent-id shape))
|
||||
page (wsh/lookup-page state)
|
||||
objects (wsh/lookup-page-objects state)
|
||||
parent (get objects (:parent-id shape))
|
||||
|
||||
;; If the target parent is a grid layout we need to pass the target cell
|
||||
target-cell (when (ctl/grid-layout? parent)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.main.data.shortcuts :as ds]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -113,18 +114,24 @@
|
||||
|
||||
(defn calculate-text-values
|
||||
[shape]
|
||||
(let [state-map (deref refs/workspace-editor-state)
|
||||
editor-state (get state-map (:id shape))]
|
||||
(let [state-map (if (features/active-feature? @st/state "text-editor/v2")
|
||||
(deref refs/workspace-v2-editor-state)
|
||||
(deref refs/workspace-editor-state))
|
||||
editor-state (get state-map (:id shape))
|
||||
editor-instance (when (features/active-feature? @st/state "text-editor/v2")
|
||||
(deref refs/workspace-editor))]
|
||||
(d/merge
|
||||
(dwt/current-root-values
|
||||
{:shape shape
|
||||
:attrs txt/root-attrs})
|
||||
(dwt/current-paragraph-values
|
||||
{:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/paragraph-attrs})
|
||||
(dwt/current-text-values
|
||||
{:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/text-node-attrs}))))
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
(.error js/console cause)
|
||||
(rx/empty)))
|
||||
|
||||
(rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
|
||||
(rx/tap #(l/dbg :hint "thumbnail updated" :elapsed (dm/str (tp) "ms")))
|
||||
|
||||
;; We cancel all the stream if user starts editing while
|
||||
;; thumbnail is generating
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "rename-version"}))))))
|
||||
|
||||
(defn restore-version
|
||||
[project-id file-id id]
|
||||
[project-id file-id id origin]
|
||||
(dm/assert! (uuid? project-id))
|
||||
(dm/assert! (uuid? file-id))
|
||||
(dm/assert! (uuid? id))
|
||||
@@ -132,7 +132,17 @@
|
||||
(rx/take 1)
|
||||
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
|
||||
(rx/map #(dw/initialize-file project-id file-id)))
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version"}))))))
|
||||
(case origin
|
||||
:version
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
|
||||
|
||||
:snapshot
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-autosave"}))
|
||||
|
||||
:plugin
|
||||
(rx/of (ptk/event ::ev/event {::ev/name "restore-version-plugin"}))
|
||||
|
||||
(rx/empty))))))
|
||||
|
||||
(defn delete-version
|
||||
[file-id id]
|
||||
|
||||
@@ -33,10 +33,13 @@
|
||||
|
||||
(defn get-team-enabled-features
|
||||
[state]
|
||||
(-> global-enabled-features
|
||||
(set/union (:features-runtime state #{}))
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union (:features-team state #{}))))
|
||||
(let [runtime-features (:features-runtime state #{})
|
||||
team-features (->> (:features-team state #{})
|
||||
(into #{} cfeat/xf-remove-ephimeral))]
|
||||
(-> global-enabled-features
|
||||
(set/union runtime-features)
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))))
|
||||
|
||||
(def features-ref
|
||||
(l/derived get-team-enabled-features st/state =))
|
||||
@@ -124,9 +127,9 @@
|
||||
(let [features (get-team-enabled-features state)]
|
||||
(if (contains? features "render-wasm/v1")
|
||||
(render.wasm/initialize true)
|
||||
(render.wasm/initialize false)))
|
||||
(render.wasm/initialize false))
|
||||
|
||||
(log/trc :hint "initialized features"
|
||||
:team (str/join "," (:features-team state))
|
||||
:runtime (str/join "," (:features-runtime state)))))))
|
||||
(log/inf :hint "initialized"
|
||||
:enabled (str/join "," features)
|
||||
:runtime (str/join "," (:features-runtime state))))))))
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.common.types.shape-tree :as ctt]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.config :as cf]
|
||||
[app.main.data.workspace.state-helpers :as wsh]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.tokens.token-set :as wtts]
|
||||
@@ -582,7 +583,8 @@
|
||||
[object-id]
|
||||
(l/derived
|
||||
(fn [state]
|
||||
(dm/get-in state [:workspace-thumbnails object-id]))
|
||||
(some-> (dm/get-in state [:workspace-thumbnails object-id])
|
||||
(cf/resolve-media)))
|
||||
st/state))
|
||||
|
||||
(def workspace-text-modifier
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
;; used to render thumbnails on assets panel.
|
||||
(mf/defc component-svg
|
||||
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
[{:keys [objects root-shape show-grids? zoom class] :or {zoom 1} :as props}]
|
||||
[{:keys [objects root-shape show-grids? is-hidden zoom class] :or {zoom 1} :as props}]
|
||||
(when root-shape
|
||||
(let [root-shape-id (:id root-shape)
|
||||
include-metadata (mf/use-ctx export/include-metadata-ctx)
|
||||
@@ -381,13 +381,14 @@
|
||||
:xmlns:penpot (when include-metadata "https://penpot.app/xmlns")
|
||||
:fill "none"}
|
||||
|
||||
[:*
|
||||
[:> shape-container {:shape root-shape'}
|
||||
[:& (mf/provider muc/is-component?) {:value true}
|
||||
[:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
|
||||
(when-not is-hidden
|
||||
[:*
|
||||
[:> shape-container {:shape root-shape'}
|
||||
[:& (mf/provider muc/is-component?) {:value true}
|
||||
[:& root-shape-wrapper {:shape root-shape' :view-box vbox}]]]
|
||||
|
||||
(when show-grids?
|
||||
[:& empty-grids {:root-shape-id root-shape-id :objects objects}])]])))
|
||||
(when show-grids?
|
||||
[:& empty-grids {:root-shape-id root-shape-id :objects objects}])])])))
|
||||
|
||||
(mf/defc component-svg-thumbnail
|
||||
{::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.config :as cf]
|
||||
[app.main.ui.icons :as i]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -31,7 +32,7 @@
|
||||
i/flex-grid
|
||||
|
||||
:else
|
||||
i/board)
|
||||
(if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board))
|
||||
;; TODO -> THUMBNAIL ICON
|
||||
:image i/img
|
||||
:line (if (cts/has-images? shape) i/img i/path)
|
||||
@@ -56,7 +57,7 @@
|
||||
(if main-instance?
|
||||
i/component
|
||||
(case type
|
||||
:frame i/board
|
||||
:frame (if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board)
|
||||
:image i/img
|
||||
:shape i/path
|
||||
:text i/text
|
||||
|
||||
@@ -73,7 +73,9 @@
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :team-form}
|
||||
[{:keys [team] :as props}]
|
||||
(let [initial (mf/use-memo (fn [] (or team {})))
|
||||
(let [initial (mf/use-memo (fn []
|
||||
(or (some-> team (select-keys [:name :id]))
|
||||
{})))
|
||||
form (fm/use-form :schema schema:team-form
|
||||
:initial initial)
|
||||
handle-keydown
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
(def ^:icon arrow (icon-xref :arrow))
|
||||
(def ^:icon asc-sort (icon-xref :asc-sort))
|
||||
(def ^:icon board (icon-xref :board))
|
||||
(def ^:icon board-2 (icon-xref :board-2))
|
||||
(def ^:icon boards-thumbnail (icon-xref :boards-thumbnail))
|
||||
(def ^:icon boolean-difference (icon-xref :boolean-difference))
|
||||
(def ^:icon boolean-exclude (icon-xref :boolean-exclude))
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"The introduction of our brand new Plugin system allows you to access even richer ecosystem of capabilities."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"We are beyond excitement about how this will further involve the Penpot community in building the best design and prototyping platform."]
|
||||
"We are beyond excited about how this will further involve the Penpot community in building the best design and prototyping platform."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
@@ -69,7 +69,7 @@
|
||||
"Penpot Plugins encourage developers to easily customize and expand the platform using standard web technologies like JavaScript, CSS, and HTML."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Find everything you need in ouor full comprehensive documentation to start building your plugins now!"]]
|
||||
"Find everything you need in our full comprehensive documentation to start building your plugins now!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
@@ -101,7 +101,7 @@
|
||||
"Be sure to keep an eye on our evolving " [:a {:href "https://penpot.app/penpothub" :target "_blank"} "Penpot Hub"] " to pick the ones that are best suited to enhance your workflow."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This is just the beginning of a myriad of possibilities. Let’s build this community together <3."]]
|
||||
"This is just the beginning of a myriad of possibilities. Let’s build this community together ❤️."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
bounds (mf/with-memo [bounds points]
|
||||
(or bounds (gsb/get-frame-bounds shape)))
|
||||
|
||||
thumb (:thumbnail shape)
|
||||
thumb (cf/resolve-media (:thumbnail-id shape))
|
||||
|
||||
debug? (dbg/enabled? :thumbnails)
|
||||
safari? (cf/check-browser? :safari)
|
||||
@@ -171,7 +171,7 @@
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (unchecked-get props "shape")]
|
||||
(when ^boolean (:thumbnail shape)
|
||||
(when ^boolean (:thumbnail-id shape)
|
||||
[:> frame-container props
|
||||
[:> frame-thumbnail-image props]])))
|
||||
|
||||
|
||||
@@ -49,11 +49,16 @@
|
||||
|
||||
(defn generate-paragraph-styles
|
||||
[_shape data]
|
||||
(let [line-height (:line-height data 1.2)
|
||||
(let [line-height (:line-height data)
|
||||
line-height
|
||||
(if (and (some? line-height) (not= "" line-height))
|
||||
line-height
|
||||
(:line-height txt/default-text-attrs))
|
||||
|
||||
text-align (:text-align data "start")
|
||||
base #js {;; Fix a problem when exporting HTML
|
||||
:fontSize 0 ;;(str (:font-size data (:font-size txt/default-text-attrs)) "px")
|
||||
:lineHeight (:line-height data (:line-height txt/default-text-attrs))
|
||||
:lineHeight line-height
|
||||
:margin 0}]
|
||||
|
||||
(cond-> base
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.versions :as dwv]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -524,6 +525,32 @@
|
||||
(when (kbd/enter? event)
|
||||
(on-add-shared event))))
|
||||
|
||||
on-show-version-history
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [_]
|
||||
(st/emit! (dw/toggle-layout-flag :document-history))))
|
||||
|
||||
on-show-version-history-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-show-version-history)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-show-version-history event))))
|
||||
|
||||
on-pin-version
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
(fn [_]
|
||||
(st/emit! (dwv/create-version file-id))))
|
||||
|
||||
on-pin-version-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps on-pin-version)
|
||||
(fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(on-pin-version event))))
|
||||
|
||||
on-export-shapes
|
||||
(mf/use-fn #(st/emit! (de/show-workspace-export-dialog {:origin "workspace:menu"})))
|
||||
|
||||
@@ -575,14 +602,34 @@
|
||||
:on-click on-remove-shared
|
||||
:on-key-down on-remove-shared-key-down
|
||||
:id "file-menu-remove-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]])
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.unpublish-shared")]])
|
||||
|
||||
(when can-edit
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-add-shared
|
||||
:on-key-down on-add-shared-key-down
|
||||
:id "file-menu-add-shared"}
|
||||
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]]))
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.add-shared")]]))
|
||||
|
||||
[:div {:class (stl/css :separator)}]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-pin-version
|
||||
:on-key-down on-pin-version-key-down
|
||||
:id "file-menu-show-version-history"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.create-version-menu")]]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-show-version-history
|
||||
:on-key-down on-show-version-history-key-down
|
||||
:id "file-menu-show-version-history"}
|
||||
[:span {:class (stl/css :item-name)}
|
||||
(tr "dashboard.show-version-history")]]
|
||||
|
||||
[:div {:class (stl/css :separator)}]
|
||||
|
||||
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
|
||||
:on-click on-export-shapes
|
||||
|
||||
@@ -43,9 +43,12 @@
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-top: $s-8;
|
||||
border-top: $s-1 solid var(--color-background-quaternary);
|
||||
height: $s-4;
|
||||
border-top: $s-1 solid var(--color-background-secondary);
|
||||
left: calc(-1 * $s-4);
|
||||
margin-top: $s-8;
|
||||
position: relative;
|
||||
width: calc(100% + $s-8);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
[app.util.text.content.styles :as styles]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- gen-name
|
||||
[editor]
|
||||
(when (some? editor)
|
||||
(let [editor-root (.-root editor)
|
||||
result (.-textContent editor-root)]
|
||||
(when (not= result "") result))))
|
||||
|
||||
(defn- initialize-event-handlers
|
||||
"Internal editor events handler initializer/destructor"
|
||||
[shape-id content selection-ref editor-ref container-ref]
|
||||
@@ -51,6 +58,8 @@
|
||||
instance
|
||||
(dwt/create-editor editor-node options)
|
||||
|
||||
update-name? (nil? content)
|
||||
|
||||
on-key-up
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
@@ -60,7 +69,7 @@
|
||||
on-blur
|
||||
(fn []
|
||||
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
|
||||
(st/emit! (dwt/v2-update-text-shape-content shape-id content true)))
|
||||
(st/emit! (dwt/v2-update-text-shape-content shape-id content update-name? (gen-name instance))))
|
||||
|
||||
(let [container-node (mf/ref-val container-ref)]
|
||||
(dom/set-style! container-node "opacity" 0)))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.common.math :as mth]
|
||||
[app.common.text :as txt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.modifiers :as mdwm]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.fonts :as fonts]
|
||||
@@ -184,7 +185,7 @@
|
||||
(mf/use-fn
|
||||
(fn [shape node]
|
||||
;; Unique to indentify the pending state
|
||||
(let [uid (js/Symbol)]
|
||||
(let [uid (uuid/next)]
|
||||
(swap! pending-update* assoc uid (:id shape))
|
||||
(p/then
|
||||
(update-text-shape shape node)
|
||||
|
||||
@@ -237,15 +237,11 @@
|
||||
[:& comments-sidebar]
|
||||
|
||||
(true? is-history?)
|
||||
[:> tab-switcher* {:tabs #js [#js {:label "History" :id "history" :content versions-tab}
|
||||
#js {:label "Actions" :id "actions" :content history-tab}]
|
||||
:default-selected "history"
|
||||
;;:selected (name section)
|
||||
;;:on-change-tab on-tab-change
|
||||
:class (stl/css :left-sidebar-tabs)
|
||||
;;:action-button-position "start"
|
||||
;;:action-button (mf/html [:& collapse-button {:on-click handle-collapse}])
|
||||
}]
|
||||
[:> tab-switcher*
|
||||
{:tabs #js [#js {:label (tr "workspace.versions.tab.history") :id "history" :content versions-tab}
|
||||
#js {:label (tr "workspace.versions.tab.actions") :id "actions" :content history-tab}]
|
||||
:default-selected "history"
|
||||
:class (stl/css :left-sidebar-tabs)}]
|
||||
|
||||
:else
|
||||
[:> options-toolbox props])]]]))
|
||||
|
||||
@@ -113,4 +113,5 @@ $width-settings-bar-max: $s-500;
|
||||
.versions-tab {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - $s-88);
|
||||
}
|
||||
|
||||
@@ -133,23 +133,23 @@
|
||||
|
||||
options
|
||||
[{:name (tr "workspace.assets.box-filter-all")
|
||||
:id "section-all"
|
||||
:id "all"
|
||||
:handler on-section-filter-change}
|
||||
{:name (tr "workspace.assets.components")
|
||||
:id "section-components"
|
||||
:id "components"
|
||||
:handler on-section-filter-change}
|
||||
|
||||
(when (not components-v2)
|
||||
{:name (tr "workspace.assets.graphics")
|
||||
:id "section-graphics"
|
||||
:id "graphics"
|
||||
:handler on-section-filter-change})
|
||||
|
||||
{:name (tr "workspace.assets.colors")
|
||||
:id "section-colors"
|
||||
:id "colors"
|
||||
:handler on-section-filter-change}
|
||||
|
||||
{:name (tr "workspace.assets.typography")
|
||||
:id "section-typographies"
|
||||
:id "typographies"
|
||||
:handler on-section-filter-change}]]
|
||||
|
||||
[:article {:class (stl/css :assets-bar)}
|
||||
|
||||
@@ -269,17 +269,19 @@
|
||||
|
||||
(mf/defc component-item-thumbnail
|
||||
"Component that renders the thumbnail image or the original SVG."
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [file-id root-shape component container class]}]
|
||||
(let [page-id (:main-instance-page component)
|
||||
root-id (:main-instance-id component)
|
||||
{::mf/props :obj}
|
||||
[{:keys [file-id root-shape component container class is-hidden]}]
|
||||
(let [page-id (:main-instance-page component)
|
||||
root-id (:main-instance-id component)
|
||||
retry (mf/use-state 0)
|
||||
|
||||
retry (mf/use-state 0)
|
||||
thumbnail-uri*
|
||||
(mf/with-memo [file-id page-id root-id]
|
||||
(let [object-id (thc/fmt-object-id file-id page-id root-id "component")]
|
||||
(refs/workspace-thumbnail-by-id object-id)))
|
||||
|
||||
thumbnail-uri* (mf/with-memo [file-id page-id root-id]
|
||||
(let [object-id (thc/fmt-object-id file-id page-id root-id "component")]
|
||||
(refs/workspace-thumbnail-by-id object-id)))
|
||||
thumbnail-uri (mf/deref thumbnail-uri*)
|
||||
thumbnail-uri
|
||||
(mf/deref thumbnail-uri*)
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
@@ -288,7 +290,8 @@
|
||||
(when (< @retry 3)
|
||||
(inc retry))))]
|
||||
|
||||
(if (and (some? thumbnail-uri) (contains? cf/flags :component-thumbnails))
|
||||
(if (and (some? thumbnail-uri)
|
||||
(contains? cf/flags :component-thumbnails))
|
||||
[:& component-svg-thumbnail
|
||||
{:thumbnail-uri thumbnail-uri
|
||||
:class class
|
||||
@@ -301,7 +304,8 @@
|
||||
{:root-shape root-shape
|
||||
:class class
|
||||
:objects (:objects container)
|
||||
:show-grids? true}])))
|
||||
:show-grids? true
|
||||
:is-hidden is-hidden}])))
|
||||
|
||||
(defn generate-components-menu-entries
|
||||
[shapes components-v2]
|
||||
|
||||
@@ -57,15 +57,6 @@
|
||||
component)]
|
||||
[root-shape container])))
|
||||
|
||||
|
||||
;; NOTE: We don't schedule the thumbnail generation on idle right now
|
||||
;; until we can queue and handle thumbnail batching properly.
|
||||
#_(mf/with-effect []
|
||||
(when-not (some? thumbnail-uri)
|
||||
(tm/schedule-on-idle
|
||||
#(st/emit! (dwl/update-component-thumbnail (:id component) file-id)))))
|
||||
|
||||
|
||||
(mf/defc components-item
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [component renaming listing-thumbs? selected
|
||||
@@ -180,13 +171,13 @@
|
||||
(when ^boolean dragging?
|
||||
[:div {:class (stl/css :dragging)}])]
|
||||
|
||||
(when visible?
|
||||
[:& cmm/component-item-thumbnail {:file-id file-id
|
||||
:class (stl/css-case :thumbnail true
|
||||
:asset-list-thumbnail (not listing-thumbs?))
|
||||
:root-shape root-shape
|
||||
:component component
|
||||
:container container}])])]))
|
||||
[:& cmm/component-item-thumbnail {:file-id file-id
|
||||
:class (stl/css-case :thumbnail true
|
||||
:asset-list-thumbnail (not listing-thumbs?))
|
||||
:root-shape root-shape
|
||||
:component component
|
||||
:container container
|
||||
:is-hidden (not visible?)}]])]))
|
||||
|
||||
(mf/defc components-group
|
||||
{::mf/wrap-props false}
|
||||
|
||||
@@ -74,14 +74,13 @@
|
||||
|
||||
(mf/defc file-library-content
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [file local? open-status-ref on-clear-selection]}]
|
||||
[{:keys [file local? open-status-ref on-clear-selection filters]}]
|
||||
(let [components-v2 (mf/use-ctx ctx/components-v2)
|
||||
open-status (mf/deref open-status-ref)
|
||||
|
||||
file-id (:id file)
|
||||
project-id (:project-id file)
|
||||
|
||||
filters (mf/use-ctx cmm/assets-filters)
|
||||
filters-section (:section filters)
|
||||
|
||||
filters-term (:term filters)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -154,7 +155,7 @@
|
||||
:circle i/elipse
|
||||
:text i/text
|
||||
:path i/path
|
||||
:frame i/board
|
||||
:frame (if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board)
|
||||
:group i/group
|
||||
:color i/drop-icon
|
||||
:typography i/text-palette
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -335,7 +336,7 @@
|
||||
:on-click add-filter}
|
||||
[:div {:class (stl/css :filter-menu-item-name-wrapper)}
|
||||
[:span {:class (stl/css :filter-menu-item-icon)}
|
||||
i/board]
|
||||
(if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board)]
|
||||
[:span {:class (stl/css :filter-menu-item-name)}
|
||||
(tr "workspace.sidebar.layers.frames")]]
|
||||
|
||||
|
||||
@@ -134,7 +134,8 @@
|
||||
time (dt/timeago (:created-at entry) {:locale locale})]
|
||||
[:span {:class (stl/css :date)} time])]]
|
||||
|
||||
[:> icon-button* {:variant "ghost"
|
||||
[:> icon-button* {:class (stl/css :version-entry-options)
|
||||
:variant "ghost"
|
||||
:aria-label (tr "workspace.versions.version-menu")
|
||||
:on-click handle-open-menu
|
||||
:icon "menu"}]]
|
||||
@@ -274,10 +275,11 @@
|
||||
(fn [id label]
|
||||
(st/emit! (dwv/rename-version file-id id label))))
|
||||
|
||||
|
||||
handle-restore-version
|
||||
(mf/use-fn
|
||||
(mf/deps project-id file-id)
|
||||
(fn [id]
|
||||
(fn [origin id]
|
||||
(st/emit!
|
||||
(ntf/dialog
|
||||
:content (tr "workspace.versions.restore-warning")
|
||||
@@ -287,9 +289,21 @@
|
||||
:callback #(st/emit! (ntf/hide))}
|
||||
{:label (tr "labels.restore")
|
||||
:type :primary
|
||||
:callback #(st/emit! (dwv/restore-version project-id file-id id))}]
|
||||
:callback #(st/emit! (dwv/restore-version project-id file-id id origin))}]
|
||||
:tag :restore-dialog))))
|
||||
|
||||
handle-restore-version-pinned
|
||||
(mf/use-fn
|
||||
(mf/deps handle-restore-version)
|
||||
(fn [id]
|
||||
(handle-restore-version :version id)))
|
||||
|
||||
handle-restore-version-snapshot
|
||||
(mf/use-fn
|
||||
(mf/deps handle-restore-version)
|
||||
(fn [id]
|
||||
(handle-restore-version :snapshot id)))
|
||||
|
||||
handle-delete-version
|
||||
(mf/use-fn
|
||||
(mf/deps file-id)
|
||||
@@ -362,7 +376,7 @@
|
||||
:editing? (= (:id entry) editing)
|
||||
:profile (get users (:profile-id entry))
|
||||
:on-rename-version handle-rename-version
|
||||
:on-restore-version handle-restore-version
|
||||
:on-restore-version handle-restore-version-pinned
|
||||
:on-delete-version handle-delete-version}]
|
||||
|
||||
:snapshot
|
||||
@@ -371,7 +385,7 @@
|
||||
:entry entry
|
||||
:is-expanded (contains? @expanded idx-entry)
|
||||
:on-toggle-expand handle-toggle-expand
|
||||
:on-restore-snapshot handle-restore-version
|
||||
:on-restore-snapshot handle-restore-version-snapshot
|
||||
:on-pin-snapshot handle-pin-version}]
|
||||
|
||||
nil))])])]))
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
.version-toolbox {
|
||||
padding: $s-8;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.versions-entry-empty {
|
||||
@@ -49,6 +53,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $s-6;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.version-entry {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.media :as cm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
@@ -146,7 +147,7 @@
|
||||
:on-click select-drawtool
|
||||
:data-tool "frame"
|
||||
:data-testid "artboard-btn"}
|
||||
i/board]]
|
||||
(if (cf/external-feature-flag "boards-01" "test") i/board-2 i/board)]]
|
||||
[:li
|
||||
[:button
|
||||
{:title (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
(and (some? drawing-obj) (= :path (:type drawing-obj))))
|
||||
node-editing? (and edition (= :path (get-in base-objects [edition :type])))
|
||||
text-editing? (and edition (= :text (get-in base-objects [edition :type])))
|
||||
|
||||
grid-editing? (and edition (ctl/grid-layout? base-objects edition))
|
||||
|
||||
mode-inspect? (= options-mode :inspect)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
:else
|
||||
(let [project-id (:current-project-id @st/state)]
|
||||
(st/emit! (dwv/restore-version project-id $file $version)))))
|
||||
(st/emit! (dwv/restore-version project-id $file $version :plugin)))))
|
||||
|
||||
(remove
|
||||
[_]
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
;; height: number;
|
||||
;; mtype?: string;
|
||||
;; id: string;
|
||||
;; keepApectRatio?: boolean;
|
||||
;; keepAspectRatio?: boolean;
|
||||
;; };
|
||||
(defn format-image
|
||||
[{:keys [name width height mtype id keep-aspect-ratio] :as image}]
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
;; height: number;
|
||||
;; mtype?: string;
|
||||
;; id: string;
|
||||
;; keepApectRatio?: boolean;
|
||||
;; keepAspectRatio?: boolean;
|
||||
;;}
|
||||
(defn parse-image-data
|
||||
[^js image-data]
|
||||
@@ -77,7 +77,7 @@
|
||||
:width (obj/get image-data "width")
|
||||
:height (obj/get image-data "height")
|
||||
:mtype (obj/get image-data "mtype")
|
||||
:keep-aspect-ratio (obj/get image-data "keepApectRatio")})))
|
||||
:keep-aspect-ratio (obj/get image-data "keepAspectRatio")})))
|
||||
|
||||
;; export type Gradient = {
|
||||
;; type: 'linear' | 'radial';
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.types.shape.impl :as ctsi]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.util.object :as obj]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn initialize
|
||||
@@ -28,21 +27,24 @@
|
||||
|
||||
(defn create-shape
|
||||
[id]
|
||||
(let [buffer (uuid/uuid->u32 id)]
|
||||
(._create_shape ^js internal-module (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))))
|
||||
(let [buffer (uuid/uuid->u32 id)
|
||||
create-shape (unchecked-get internal-module "_create_shape")]
|
||||
(^function create-shape (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))))
|
||||
|
||||
(defn use-shape
|
||||
[id]
|
||||
(let [buffer (uuid/uuid->u32 id)]
|
||||
(._use_shape ^js internal-module (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))))
|
||||
(let [buffer (uuid/uuid->u32 id)
|
||||
use-shape (unchecked-get internal-module "_use_shape")]
|
||||
(^function use-shape (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))))
|
||||
|
||||
(defn set-shape-selrect
|
||||
[selrect]
|
||||
(let [x1 (:x1 selrect)
|
||||
y1 (:y1 selrect)
|
||||
x2 (:x2 selrect)
|
||||
y2 (:y2 selrect)]
|
||||
(._set_shape_selrect ^js internal-module x1 y1 x2 y2)))
|
||||
y2 (:y2 selrect)
|
||||
set-shape-selrect (unchecked-get internal-module "_set_shape_selrect")]
|
||||
(^function set-shape-selrect x1 y1 x2 y2)))
|
||||
|
||||
(defn set-shape-transform
|
||||
[transform]
|
||||
@@ -51,27 +53,33 @@
|
||||
c (:c transform)
|
||||
d (:d transform)
|
||||
e (:e transform)
|
||||
f (:f transform)]
|
||||
(._set_shape_transform ^js internal-module a b c d e f)))
|
||||
f (:f transform)
|
||||
set-shape-transform (unchecked-get internal-module "_set_shape_transform")]
|
||||
(^function set-shape-transform a b c d e f)))
|
||||
|
||||
(defn set-shape-rotation
|
||||
[rotation]
|
||||
(._set_shape_rotation ^js internal-module rotation))
|
||||
(let [set-shape-rotation (unchecked-get internal-module "_set_shape_rotation")]
|
||||
(^function set-shape-rotation rotation)))
|
||||
|
||||
(defn set-shape-children
|
||||
[shape_ids]
|
||||
(._clear_shape_children ^js internal-module)
|
||||
(doseq [id shape_ids]
|
||||
(let [buffer (uuid/uuid->u32 id)]
|
||||
(._add_shape_child ^js internal-module (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3)))))
|
||||
(let [clear-shape-children (unchecked-get internal-module "_clear_shape_children")
|
||||
add-shape-child (unchecked-get internal-module "_add_shape_child")]
|
||||
(^function clear-shape-children)
|
||||
(doseq [id shape_ids]
|
||||
(let [buffer (uuid/uuid->u32 id)]
|
||||
(^function add-shape-child (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))))))
|
||||
|
||||
(defn set-shape-fills
|
||||
[fills]
|
||||
(._clear_shape_fills ^js internal-module)
|
||||
(doseq [fill (filter #(contains? % :fill-color) fills)]
|
||||
(let [a (:fill-opacity fill)
|
||||
[r g b] (cc/hex->rgb (:fill-color fill))]
|
||||
(._add_shape_solid_fill ^js internal-module r g b a))))
|
||||
(let [clear-shape-fills (unchecked-get internal-module "_clear_shape_fills")
|
||||
add-shape-fill (unchecked-get internal-module "_add_shape_solid_fill")]
|
||||
(^function clear-shape-fills)
|
||||
(doseq [fill (filter #(contains? % :fill-color) fills)]
|
||||
(let [a (:fill-opacity fill)
|
||||
[r g b] (cc/hex->rgb (:fill-color fill))]
|
||||
(^function add-shape-fill r g b a)))))
|
||||
|
||||
(defn set-objects
|
||||
[objects]
|
||||
@@ -98,9 +106,10 @@
|
||||
[zoom vbox]
|
||||
(js/requestAnimationFrame
|
||||
(fn []
|
||||
(let [pan-x (- (dm/get-prop vbox :x))
|
||||
pan-y (- (dm/get-prop vbox :y))]
|
||||
(._draw_all_shapes ^js internal-module zoom pan-x pan-y)))))
|
||||
(let [pan-x (- (dm/get-prop vbox :x))
|
||||
pan-y (- (dm/get-prop vbox :y))
|
||||
draw-all-shapes (unchecked-get internal-module "_draw_all_shapes")]
|
||||
(^function draw-all-shapes zoom pan-x pan-y)))))
|
||||
|
||||
(defn cancel-draw
|
||||
[frame-id]
|
||||
@@ -129,12 +138,10 @@
|
||||
handle (.registerContext ^js gl context #js {"majorVersion" 2})]
|
||||
(.makeContextCurrent ^js gl handle)
|
||||
;; Initialize Skia
|
||||
(init-fn (.-width ^js canvas)
|
||||
(.-height ^js canvas))
|
||||
(^function init-fn (.-width ^js canvas)
|
||||
(.-height ^js canvas))
|
||||
(set! (.-width canvas) (.-clientWidth ^js canvas))
|
||||
(set! (.-height canvas) (.-clientHeight ^js canvas))
|
||||
|
||||
(obj/set! js/window "shape_list" (fn [] ((unchecked-get internal-module "_shape_list"))))))
|
||||
(set! (.-height canvas) (.-clientHeight ^js canvas))))
|
||||
|
||||
(defonce module
|
||||
(if (exists? js/dynamicImport)
|
||||
|
||||
@@ -148,9 +148,7 @@
|
||||
(mf/set-ref-val! internal-state initial))
|
||||
|
||||
(mf/with-effect [initial]
|
||||
(if (fn? initial)
|
||||
(swap! form-mutator update :data merge (initial))
|
||||
(swap! form-mutator update :data merge initial)))
|
||||
(swap! form-mutator d/deep-merge initial))
|
||||
|
||||
form-mutator))
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
(defn v2-closest-text-editor-content
|
||||
[target]
|
||||
(.closest ^js target ".text-editor-content"))
|
||||
(.closest ^js target "[data-itype=\"editor\"]"))
|
||||
|
||||
(defn closest-text-editor-content
|
||||
[target]
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
(defn v2-get-text-editor-content
|
||||
[]
|
||||
(dom/get-element-by-class "text-editor-content"))
|
||||
(dom/query "[data-itype=\"editor\"]"))
|
||||
|
||||
(defn get-text-editor-content
|
||||
[]
|
||||
|
||||
71
frontend/text-editor/.editorconfig
Normal file
71
frontend/text-editor/.editorconfig
Normal file
@@ -0,0 +1,71 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
end_of_line = lf
|
||||
|
||||
insert_final_newline = true
|
||||
|
||||
trim_trailing_whitespace = true
|
||||
cpp_indent_braces=false
|
||||
cpp_indent_multi_line_relative_to=innermost_parenthesis
|
||||
cpp_indent_within_parentheses=indent
|
||||
cpp_indent_preserve_within_parentheses=false
|
||||
cpp_indent_case_labels=false
|
||||
cpp_indent_case_contents=true
|
||||
cpp_indent_case_contents_when_block=false
|
||||
cpp_indent_lambda_braces_when_parameter=true
|
||||
cpp_indent_goto_labels=one_left
|
||||
cpp_indent_preprocessor=leftmost_column
|
||||
cpp_indent_access_specifiers=false
|
||||
cpp_indent_namespace_contents=true
|
||||
cpp_indent_preserve_comments=false
|
||||
cpp_new_line_before_open_brace_namespace=ignore
|
||||
cpp_new_line_before_open_brace_type=ignore
|
||||
cpp_new_line_before_open_brace_function=ignore
|
||||
cpp_new_line_before_open_brace_block=ignore
|
||||
cpp_new_line_before_open_brace_lambda=ignore
|
||||
cpp_new_line_scope_braces_on_separate_lines=false
|
||||
cpp_new_line_close_brace_same_line_empty_type=false
|
||||
cpp_new_line_close_brace_same_line_empty_function=false
|
||||
cpp_new_line_before_catch=true
|
||||
cpp_new_line_before_else=true
|
||||
cpp_new_line_before_while_in_do_while=false
|
||||
cpp_space_before_function_open_parenthesis=remove
|
||||
cpp_space_within_parameter_list_parentheses=false
|
||||
cpp_space_between_empty_parameter_list_parentheses=false
|
||||
cpp_space_after_keywords_in_control_flow_statements=true
|
||||
cpp_space_within_control_flow_statement_parentheses=false
|
||||
cpp_space_before_lambda_open_parenthesis=false
|
||||
cpp_space_within_cast_parentheses=false
|
||||
cpp_space_after_cast_close_parenthesis=false
|
||||
cpp_space_within_expression_parentheses=false
|
||||
cpp_space_before_block_open_brace=true
|
||||
cpp_space_between_empty_braces=false
|
||||
cpp_space_before_initializer_list_open_brace=false
|
||||
cpp_space_within_initializer_list_braces=true
|
||||
cpp_space_preserve_in_initializer_list=true
|
||||
cpp_space_before_open_square_bracket=false
|
||||
cpp_space_within_square_brackets=false
|
||||
cpp_space_before_empty_square_brackets=false
|
||||
cpp_space_between_empty_square_brackets=false
|
||||
cpp_space_group_square_brackets=true
|
||||
cpp_space_within_lambda_brackets=false
|
||||
cpp_space_between_empty_lambda_brackets=false
|
||||
cpp_space_before_comma=false
|
||||
cpp_space_after_comma=true
|
||||
cpp_space_remove_around_member_operators=true
|
||||
cpp_space_before_inheritance_colon=true
|
||||
cpp_space_before_constructor_colon=true
|
||||
cpp_space_remove_before_semicolon=true
|
||||
cpp_space_after_semicolon=false
|
||||
cpp_space_remove_around_unary_operator=true
|
||||
cpp_space_around_binary_operator=insert
|
||||
cpp_space_around_assignment_operator=insert
|
||||
cpp_space_pointer_reference_alignment=left
|
||||
cpp_space_around_ternary_operator=insert
|
||||
cpp_wrap_preserve_blocks=one_liners
|
||||
342
frontend/text-editor/.gitignore
vendored
Normal file
342
frontend/text-editor/.gitignore
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/linux,osx,windows,vim,emacs,sublimetext,visualstudiocode,node
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,osx,windows,vim,emacs,sublimetext,visualstudiocode,node
|
||||
|
||||
### Emacs ###
|
||||
# -*- mode: gitignore; -*-
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# Org-mode
|
||||
.org-id-locations
|
||||
*_archive
|
||||
|
||||
# flymake-mode
|
||||
*_flymake.*
|
||||
|
||||
# eshell files
|
||||
/eshell/history
|
||||
/eshell/lastdir
|
||||
|
||||
# elpa packages
|
||||
/elpa/
|
||||
|
||||
# reftex files
|
||||
*.rel
|
||||
|
||||
# AUCTeX auto folder
|
||||
/auto/
|
||||
|
||||
# cask packages
|
||||
.cask/
|
||||
dist/
|
||||
|
||||
# Flycheck
|
||||
flycheck_*.el
|
||||
|
||||
# server auth directory
|
||||
/server/
|
||||
|
||||
# projectiles files
|
||||
.projectile
|
||||
|
||||
# directory configuration
|
||||
.dir-locals.el
|
||||
|
||||
# network security
|
||||
/network-security.data
|
||||
|
||||
|
||||
### Linux ###
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
### Node Patch ###
|
||||
# Serverless Webpack directories
|
||||
.webpack/
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
# SvelteKit build / generate output
|
||||
.svelte-kit
|
||||
|
||||
### OSX ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### SublimeText ###
|
||||
# Cache files for Sublime Text
|
||||
*.tmlanguage.cache
|
||||
*.tmPreferences.cache
|
||||
*.stTheme.cache
|
||||
|
||||
# Workspace files are user-specific
|
||||
*.sublime-workspace
|
||||
|
||||
# Project files should be checked into the repository, unless a significant
|
||||
# proportion of contributors will probably not be using Sublime Text
|
||||
# *.sublime-project
|
||||
|
||||
# SFTP configuration file
|
||||
sftp-config.json
|
||||
sftp-config-alt*.json
|
||||
|
||||
# Package control specific files
|
||||
Package Control.last-run
|
||||
Package Control.ca-list
|
||||
Package Control.ca-bundle
|
||||
Package Control.system-ca-bundle
|
||||
Package Control.cache/
|
||||
Package Control.ca-certs/
|
||||
Package Control.merged-ca-bundle
|
||||
Package Control.user-ca-bundle
|
||||
oscrypto-ca-bundle.crt
|
||||
bh_unicode_properties.cache
|
||||
|
||||
# Sublime-github package stores a github token in this file
|
||||
# https://packagecontrol.io/packages/sublime-github
|
||||
GitHub.sublime-settings
|
||||
|
||||
### Vim ###
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/linux,osx,windows,vim,emacs,sublimetext,visualstudiocode,node
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
vite.config.js.timestamp*
|
||||
7
frontend/text-editor/.prettierrc
Normal file
7
frontend/text-editor/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
}
|
||||
106
frontend/text-editor/README.md
Normal file
106
frontend/text-editor/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Penpot Text Editor
|
||||
|
||||
## How to run it
|
||||
|
||||
### Development
|
||||
|
||||
To start the development environment run:
|
||||
|
||||
```sh
|
||||
yarn run dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
For running unit tests and running coverage:
|
||||
|
||||
```sh
|
||||
yarn run test
|
||||
yarn run coverage
|
||||
```
|
||||
|
||||
> If you want, you can run the [vitest](https://vitest.dev/) UI by running:
|
||||
|
||||
```sh
|
||||
yarn run test:ui
|
||||
```
|
||||
|
||||
## How to build it
|
||||
|
||||
The editor can be built and updated inside Penpot using the following command:
|
||||
|
||||
```sh
|
||||
PENPOT_SOURCE_PATH=/path/to/penpot/repository yarn build:update
|
||||
```
|
||||
|
||||
This command is going to search for the file located in `frontend/src/app/main/ui/workspace/shapes/text/new_editor/TextEditor.js` and update it.
|
||||
|
||||
## How it works?
|
||||
|
||||
The text editor divides the content in three elements: `root`, `paragraph` and `inline`. An `inline` in terms of content is a styled element that it is displayed in a line inside a block and an `inline` only can have one child (a Text node). A `paragraph` is a **block** element that can contain multiple `inline`s (**inline** elements).
|
||||
|
||||
```html
|
||||
<div data-itype="root">
|
||||
<div data-itype="paragraph">
|
||||
<span data-itype="inline">Hello, </span>
|
||||
<span data-itype="inline" style="font-weight: bold">World!</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
This way we only need to deal with a structure like this, where circular nodes are `HTMLElement`s and square nodes are `Text`. Also with an structure like this we have a predictable and ordered tree where we can find our position easily to do any operation (remove, insert, replace, etc).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
root((root)) --> paragraph((paragraph))
|
||||
paragraph --> inline_1((inline))
|
||||
paragraph --> inline_2((inline))
|
||||
inline_1 --> text_1[Hello, ]
|
||||
inline_2 --> text_2[World!]
|
||||
```
|
||||
|
||||
This is compatible with the way Penpot stores text content.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
root((root)) --> paragraph-set((paragraph-set))
|
||||
paragraph-set --> paragraph((paragraph))
|
||||
paragraph --> text((text))
|
||||
```
|
||||
|
||||
## How the code is organized?
|
||||
|
||||
- `editor`: contains everything related to the TextEditor. Where `TextEditor.js` is the main file where all the basic code of the editor is handled. This has been designed so that in the future, when the Web Components API is more stable and has features such as handling selection events within shadow roots we will be able to update this class with little effort.
|
||||
- `editor/clipboard`: Event handlers for clipboard events.
|
||||
- `editor/commands`: Event handlers for input events (commands) that modifies the content of the TextEditor.
|
||||
- `editor/content`: Code related to handling elements like text nodes, paragraphs, line breaks, etc. This are a series of utility functions that can perform some verifications and mutations on DOM nodes.
|
||||
- `editor/controllers`: There are two controllers; `ChangeController` that handles when a change in the content should be notified and `SelectionController` that handles operations on selections and text, this is where all the mutations on DOM nodes are performed.
|
||||
|
||||
## Implementation
|
||||
|
||||
Everything is implemented in JavaScript using `beforeinput` and `InputEvent` for the user events. `blur` and `focus` are used to handle imposter selections.
|
||||
|
||||
### Why imposter selections?
|
||||
|
||||
Normally when you click on another UI element, the current selection is replaced by the selection of the new UI element.
|
||||
|
||||
## References
|
||||
|
||||
- [InputEvent](https://w3c.github.io/input-events/#interface-InputEvent): the main event used for handling user input.
|
||||
- [Selection](https://developer.mozilla.org/en-US/docs/Web/API/Selection): for handling selections.
|
||||
- [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range): for handling range selections. <sup>1</sup>
|
||||
- [Node](https://developer.mozilla.org/en-US/docs/Web/API/Node): for operator functions like `compareDocumentPosition` or `nodeType`.
|
||||
- [Text](https://developer.mozilla.org/en-US/docs/Web/API/Range): for operator functions like `splitText`.
|
||||
- [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element): for operator functions like `after`, `before`, `append`, `remove`, `prepend`, etc.
|
||||
|
||||
<sup>1</sup>: Firefox is the only browser right now (2024-07-08) that has support for multiple selection ranges so we have to deal with this special case removing old ranges.
|
||||
|
||||
## For future reference
|
||||
|
||||
In a near future maybe we could take a lot at the [EditContext API](https://developer.mozilla.org/en-US/docs/Web/API/EditContext_API).
|
||||
|
||||
## FAQ
|
||||
|
||||
### Sometimes I receive 'TypeError: Cannot read from private field'
|
||||
|
||||
Sometimes, when you update the TextEditor source code, this exception could raise because shadow-cljs updated the code but keeps a reference to the old instance of the text editor, so the new code tries to read a private field from an old instance.
|
||||
7
frontend/text-editor/jsconfig.json
Normal file
7
frontend/text-editor/jsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
29
frontend/text-editor/package.json
Normal file
29
frontend/text-editor/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@penpot/text-editor",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "src/editor/TextEditor.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"coverage": "vitest run --coverage",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"test:watch:ui": "vitest --ui",
|
||||
"test:watch:e2e": "vitest --browser"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.45.1",
|
||||
"@types/node": "^20.14.10",
|
||||
"@vitest/browser": "^1.6.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"playwright": "^1.45.1",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.3.1",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
1
frontend/text-editor/public/javascript.svg
Normal file
1
frontend/text-editor/public/javascript.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
1
frontend/text-editor/public/vite.svg
Normal file
1
frontend/text-editor/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
32
frontend/text-editor/src/editor/Event.js
Normal file
32
frontend/text-editor/src/editor/Event.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a series of listeners.
|
||||
*
|
||||
* @param {EventTarget} target
|
||||
* @param {Object.<string, Function>} object
|
||||
* @param {EventListenerOptions} [options]
|
||||
*/
|
||||
export function addEventListeners(target, object, options) {
|
||||
Object.entries(object).forEach(([type, listener]) =>
|
||||
target.addEventListener(type, listener, options)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a series of listeners.
|
||||
*
|
||||
* @param {EventTarget} target
|
||||
* @param {Object.<string, Function>} object
|
||||
*/
|
||||
export function removeEventListeners(target, object) {
|
||||
Object.entries(object).forEach(([type, listener]) =>
|
||||
target.removeEventListener(type, listener)
|
||||
);
|
||||
}
|
||||
29
frontend/text-editor/src/editor/Event.test.js
Normal file
29
frontend/text-editor/src/editor/Event.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import { addEventListeners, removeEventListeners } from "./Event.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Event", () => {
|
||||
test("addEventListeners should add event listeners to an element using an object", () => {
|
||||
const clickSpy = vi.fn();
|
||||
const events = {
|
||||
click: clickSpy,
|
||||
};
|
||||
const element = document.createElement("div");
|
||||
addEventListeners(element, events);
|
||||
element.dispatchEvent(new Event("click"));
|
||||
expect(clickSpy).toBeCalled();
|
||||
});
|
||||
|
||||
test("removeEventListeners should remove event listeners to an element using an object", () => {
|
||||
const clickSpy = vi.fn();
|
||||
const events = {
|
||||
click: clickSpy,
|
||||
};
|
||||
const element = document.createElement("div");
|
||||
addEventListeners(element, events);
|
||||
element.dispatchEvent(new Event("click"));
|
||||
removeEventListeners(element, events);
|
||||
element.dispatchEvent(new Event("click"));
|
||||
expect(clickSpy).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
65
frontend/text-editor/src/editor/TextEditor.css
Normal file
65
frontend/text-editor/src/editor/TextEditor.css
Normal file
@@ -0,0 +1,65 @@
|
||||
::selection {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.selection-imposter-rect {
|
||||
background-color: red;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.text-editor-selection-imposter {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.text-editor-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text-editor-content {
|
||||
height: 100%;
|
||||
font-family: sourcesanspro;
|
||||
|
||||
outline: none;
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
caret-color: black;
|
||||
|
||||
/* color: transparent; */
|
||||
color: black;
|
||||
|
||||
div {
|
||||
line-height: inherit;
|
||||
user-select: text;
|
||||
|
||||
white-space: pre;
|
||||
margin: 0px;
|
||||
/* font-size: 0px; */
|
||||
}
|
||||
|
||||
span {
|
||||
line-break: auto;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.align-top[data-itype="root"] {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.align-center[data-itype="root"] {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.align-bottom[data-itype="root"] {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
545
frontend/text-editor/src/editor/TextEditor.js
Normal file
545
frontend/text-editor/src/editor/TextEditor.js
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import clipboard from "./clipboard/index.js";
|
||||
import commands from "./commands/index.js";
|
||||
import ChangeController from "./controllers/ChangeController.js";
|
||||
import SelectionController from "./controllers/SelectionController.js";
|
||||
import { createSelectionImposterFromClientRects } from "./selection/Imposter.js";
|
||||
import { addEventListeners, removeEventListeners } from "./Event.js";
|
||||
import { createRoot, createEmptyRoot } from "./content/dom/Root.js";
|
||||
import { createParagraph } from "./content/dom/Paragraph.js";
|
||||
import { createEmptyInline, createInline } from "./content/dom/Inline.js";
|
||||
import { isLineBreak } from "./content/dom/LineBreak.js";
|
||||
import LayoutType from "./layout/LayoutType.js";
|
||||
|
||||
/**
|
||||
* Text Editor.
|
||||
*/
|
||||
export class TextEditor extends EventTarget {
|
||||
/**
|
||||
* Element content editable to be used by the TextEditor
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
#element = null;
|
||||
|
||||
/**
|
||||
* Map/Dictionary of events.
|
||||
*
|
||||
* @type {Object.<string, Function>}
|
||||
*/
|
||||
#events = null;
|
||||
|
||||
/**
|
||||
* Root element that will contain the content.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
#root = null;
|
||||
|
||||
/**
|
||||
* Change controller controls when we should notify changes.
|
||||
*
|
||||
* @type {ChangeController}
|
||||
*/
|
||||
#changeController = null;
|
||||
|
||||
/**
|
||||
* Selection controller controls the current/saved selection.
|
||||
*
|
||||
* @type {SelectionController}
|
||||
*/
|
||||
#selectionController = null;
|
||||
|
||||
/**
|
||||
* Selection imposter keeps selection elements.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
#selectionImposterElement = null;
|
||||
|
||||
/**
|
||||
* Style defaults.
|
||||
*
|
||||
* @type {Object.<string, *>}
|
||||
*/
|
||||
#styleDefaults = null;
|
||||
|
||||
/**
|
||||
* FIXME: There is a weird case where the events
|
||||
* `beforeinput` and `input` have different `data` when
|
||||
* characters are deleted when the input type is
|
||||
* `insertCompositionText`.
|
||||
*/
|
||||
#fixInsertCompositionText = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
constructor(element, options) {
|
||||
super();
|
||||
if (!(element instanceof HTMLElement))
|
||||
throw new TypeError("Invalid text editor element");
|
||||
|
||||
this.#element = element;
|
||||
this.#selectionImposterElement = options?.selectionImposterElement;
|
||||
this.#events = {
|
||||
blur: this.#onBlur,
|
||||
focus: this.#onFocus,
|
||||
|
||||
paste: this.#onPaste,
|
||||
cut: this.#onCut,
|
||||
copy: this.#onCopy,
|
||||
|
||||
beforeinput: this.#onBeforeInput,
|
||||
input: this.#onInput,
|
||||
};
|
||||
this.#styleDefaults = options?.styleDefaults;
|
||||
this.#setup(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups editor properties.
|
||||
*/
|
||||
#setupElementProperties() {
|
||||
if (!this.#element.isContentEditable) {
|
||||
this.#element.contentEditable = "true";
|
||||
// In `jsdom` it isn't enough to set the attribute 'contentEditable'
|
||||
// to `true` to work.
|
||||
// FIXME: Remove this when `jsdom` implements this interface.
|
||||
if (!this.#element.isContentEditable) {
|
||||
this.#element.setAttribute("contenteditable", "true");
|
||||
}
|
||||
}
|
||||
if (this.#element.spellcheck) this.#element.spellcheck = false;
|
||||
if (this.#element.autocapitalize) this.#element.autocapitalize = false;
|
||||
if (!this.#element.autofocus) this.#element.autofocus = true;
|
||||
if (!this.#element.role || this.#element.role !== "textbox")
|
||||
this.#element.role = "textbox";
|
||||
if (this.#element.ariaAutoComplete) this.#element.ariaAutoComplete = false;
|
||||
if (!this.#element.ariaMultiLine) this.#element.ariaMultiLine = true;
|
||||
this.#element.dataset.itype = "editor";
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups the root element.
|
||||
*/
|
||||
#setupRoot() {
|
||||
this.#root = createEmptyRoot(this.#styleDefaults);
|
||||
this.#element.appendChild(this.#root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatchs a `change` event.
|
||||
*
|
||||
* @param {CustomEvent} e
|
||||
* @returns {void}
|
||||
*/
|
||||
#onChange = (e) => this.dispatchEvent(new e.constructor(e.type, e));
|
||||
|
||||
/**
|
||||
* Dispatchs a `stylechange` event.
|
||||
*
|
||||
* @param {CustomEvent} e
|
||||
* @returns {void}
|
||||
*/
|
||||
#onStyleChange = (e) => {
|
||||
if (this.#selectionImposterElement.children.length > 0) {
|
||||
// We need to recreate the selection imposter when we've
|
||||
// already have one.
|
||||
this.#createSelectionImposter();
|
||||
}
|
||||
this.dispatchEvent(new e.constructor(e.type, e));
|
||||
};
|
||||
|
||||
/**
|
||||
* Setups the elements, the properties and the
|
||||
* initial content.
|
||||
*/
|
||||
#setup(options) {
|
||||
this.#setupElementProperties();
|
||||
this.#setupRoot();
|
||||
this.#changeController = new ChangeController(this);
|
||||
this.#changeController.addEventListener("change", this.#onChange);
|
||||
this.#selectionController = new SelectionController(
|
||||
this,
|
||||
document.getSelection(),
|
||||
options
|
||||
);
|
||||
this.#selectionController.addEventListener(
|
||||
"stylechange",
|
||||
this.#onStyleChange
|
||||
);
|
||||
addEventListeners(this.#element, this.#events, {
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the selection imposter.
|
||||
*/
|
||||
#createSelectionImposter() {
|
||||
// We only create a selection imposter if there's any selection
|
||||
// and if there is a selection imposter element to attach the
|
||||
// rects.
|
||||
if (
|
||||
this.#selectionImposterElement &&
|
||||
!this.#selectionController.isCollapsed
|
||||
) {
|
||||
const rects = this.#selectionController.range?.getClientRects();
|
||||
if (rects) {
|
||||
const rect = this.#selectionImposterElement.getBoundingClientRect();
|
||||
this.#selectionImposterElement.replaceChildren(
|
||||
createSelectionImposterFromClientRects(rect, rects)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On blur we create a new FakeSelection if there's any.
|
||||
*
|
||||
* @param {FocusEvent} e
|
||||
*/
|
||||
#onBlur = (e) => {
|
||||
this.#changeController.notifyImmediately();
|
||||
this.#selectionController.saveSelection();
|
||||
this.#createSelectionImposter();
|
||||
this.dispatchEvent(new FocusEvent(e.type, e));
|
||||
};
|
||||
|
||||
/**
|
||||
* On focus we should restore the FakeSelection from the current
|
||||
* selection.
|
||||
*
|
||||
* @param {FocusEvent} e
|
||||
*/
|
||||
#onFocus = (e) => {
|
||||
this.#selectionController.restoreSelection();
|
||||
if (this.#selectionImposterElement) {
|
||||
this.#selectionImposterElement.replaceChildren();
|
||||
}
|
||||
this.dispatchEvent(new FocusEvent(e.type, e));
|
||||
};
|
||||
|
||||
/**
|
||||
* Event called when the user pastes some text into the
|
||||
* editor.
|
||||
*
|
||||
* @param {ClipboardEvent} e
|
||||
*/
|
||||
#onPaste = (e) => {
|
||||
clipboard.paste(e, this, this.#selectionController);
|
||||
this.#notifyLayout(LayoutType.FULL, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Event called when the user cuts some text from the
|
||||
* editor.
|
||||
*
|
||||
* @param {ClipboardEvent} e
|
||||
*/
|
||||
#onCut = (e) => clipboard.cut(e, this, this.#selectionController);
|
||||
|
||||
/**
|
||||
* Event called when the user copies some text from the
|
||||
* editor.
|
||||
*
|
||||
* @param {ClipboardEvent} e
|
||||
*/
|
||||
#onCopy = (e) => clipboard.copy(e, this, this.#selectionController);
|
||||
|
||||
/**
|
||||
* Event called before the DOM is modified.
|
||||
*
|
||||
* @param {InputEvent} e
|
||||
*/
|
||||
#onBeforeInput = (e) => {
|
||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.inputType === "insertCompositionText" && !e.data) {
|
||||
e.preventDefault();
|
||||
this.#fixInsertCompositionText = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(e.inputType in commands)) {
|
||||
if (e.inputType !== "insertCompositionText") {
|
||||
e.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.inputType in commands) {
|
||||
const command = commands[e.inputType];
|
||||
if (!this.#selectionController.startMutation()) {
|
||||
return;
|
||||
}
|
||||
command(e, this, this.#selectionController);
|
||||
const mutations = this.#selectionController.endMutation();
|
||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Event called after the DOM is modified.
|
||||
*
|
||||
* @param {InputEvent} e
|
||||
*/
|
||||
#onInput = (e) => {
|
||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.inputType === "insertCompositionText" && this.#fixInsertCompositionText) {
|
||||
e.preventDefault();
|
||||
this.#fixInsertCompositionText = false;
|
||||
if (e.data) {
|
||||
this.#selectionController.fixInsertCompositionText();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.inputType === "insertCompositionText" && e.data) {
|
||||
this.#notifyLayout(LayoutType.FULL, null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies that the edited texts needs layout.
|
||||
*
|
||||
* @param {'full'|'partial'} type
|
||||
* @param {CommandMutations} mutations
|
||||
*/
|
||||
#notifyLayout(type = LayoutType.FULL, mutations) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("needslayout", {
|
||||
detail: {
|
||||
type: type,
|
||||
mutations: mutations,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root element that contains all the paragraphs.
|
||||
*
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
get root() {
|
||||
return this.#root;
|
||||
}
|
||||
|
||||
set root(newRoot) {
|
||||
const previousRoot = this.#root;
|
||||
this.#root = newRoot;
|
||||
previousRoot.replaceWith(newRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Element that contains the root and that has the
|
||||
* contenteditable attribute.
|
||||
*
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
get element() {
|
||||
return this.#element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the content is in an empty state.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isEmpty() {
|
||||
return (
|
||||
this.#root.children.length === 1 &&
|
||||
this.#root.firstElementChild.children.length === 1 &&
|
||||
isLineBreak(this.#root.firstElementChild.firstElementChild.firstChild)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the amount of paragraphs in the current content.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get numParagraphs() {
|
||||
return this.#root.children.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS Style declaration for the current inline. From here we
|
||||
* can infer root, paragraph and inline declarations.
|
||||
*
|
||||
* @type {CSSStyleDeclaration}
|
||||
*/
|
||||
get currentStyle() {
|
||||
return this.#selectionController.currentStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the element
|
||||
*/
|
||||
focus() {
|
||||
return this.#element.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurs the element
|
||||
*/
|
||||
blur() {
|
||||
return this.#element.blur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new root.
|
||||
*
|
||||
* @param {...any} args
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
createRoot(...args) {
|
||||
return createRoot(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new paragraph.
|
||||
*
|
||||
* @param {...any} args
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
createParagraph(...args) {
|
||||
return createParagraph(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new inline from a string.
|
||||
*
|
||||
* @param {string} text
|
||||
* @param {Object.<string,*>|CSSStyleDeclaration} styles
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
createInlineFromString(text, styles) {
|
||||
if (text === "") {
|
||||
return createEmptyInline(styles);
|
||||
}
|
||||
return createInline(new Text(text), styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new inline.
|
||||
*
|
||||
* @param {...any} args
|
||||
* @returns {HTMLSpanElement}
|
||||
*/
|
||||
createInline(...args) {
|
||||
return createInline(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the current styles to the selection or
|
||||
* the current DOM node at the caret.
|
||||
*
|
||||
* @param {*} styles
|
||||
*/
|
||||
applyStylesToSelection(styles) {
|
||||
this.#selectionController.startMutation();
|
||||
this.#selectionController.applyStyles(styles);
|
||||
const mutations = this.#selectionController.endMutation();
|
||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
||||
this.#changeController.notifyImmediately();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all content.
|
||||
*/
|
||||
selectAll() {
|
||||
this.#selectionController.selectAll();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves cursor to end.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
cursorToEnd() {
|
||||
this.#selectionController.cursorToEnd();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes everything.
|
||||
*/
|
||||
dispose() {
|
||||
this.#changeController.removeEventListener("change", this.#onChange);
|
||||
this.#changeController.dispose();
|
||||
this.#changeController = null;
|
||||
this.#selectionController.removeEventListener(
|
||||
"stylechange",
|
||||
this.#onStyleChange
|
||||
);
|
||||
this.#selectionController.dispose();
|
||||
this.#selectionController = null;
|
||||
removeEventListeners(this.#element, this.#events);
|
||||
this.#element = null;
|
||||
this.#root = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function isEditor(instance) {
|
||||
return (instance instanceof TextEditor);
|
||||
}
|
||||
|
||||
/* Convenience function based API for Text Editor */
|
||||
export function getRoot(instance) {
|
||||
if (isEditor(instance)) {
|
||||
return instance.root;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setRoot(instance, root) {
|
||||
if (isEditor(instance)) {
|
||||
instance.root = root;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function create(element, options) {
|
||||
return new TextEditor(element, {...options});
|
||||
}
|
||||
|
||||
export function getCurrentStyle(instance) {
|
||||
if (isEditor(instance)) {
|
||||
return instance.currentStyle;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyStylesToSelection(instance, styles) {
|
||||
if (isEditor(instance)) {
|
||||
return instance.applyStylesToSelection(styles);
|
||||
}
|
||||
}
|
||||
|
||||
export function dispose(instance) {
|
||||
if (isEditor(instance)) {
|
||||
instance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextEditor;
|
||||
100
frontend/text-editor/src/editor/TextEditor.test.js
Normal file
100
frontend/text-editor/src/editor/TextEditor.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { TextEditor } from "./TextEditor.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("TextEditor", () => {
|
||||
test("Creating TextEditor without element should throw", () => {
|
||||
expect(() => new TextEditor()).toThrowError("Invalid text editor element");
|
||||
});
|
||||
|
||||
test("Creating TextEditor with element should success", () => {
|
||||
expect(new TextEditor(document.createElement("div"))).toBeInstanceOf(
|
||||
TextEditor,
|
||||
);
|
||||
});
|
||||
|
||||
test("isEmpty should return true when editor is empty", () => {
|
||||
const textEditor = new TextEditor(document.createElement("div"));
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
expect(textEditor.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
test("Num paragraphs should return 1 when empty", () => {
|
||||
const textEditor = new TextEditor(document.createElement("div"));
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
expect(textEditor.numParagraphs).toBe(1);
|
||||
});
|
||||
|
||||
test("Num paragraphs should return the number of paragraphs", () => {
|
||||
const textEditor = new TextEditor(document.createElement("div"));
|
||||
textEditor.root = textEditor.createRoot([
|
||||
textEditor.createParagraph([
|
||||
textEditor.createInlineFromString("Hello, World!"),
|
||||
]),
|
||||
textEditor.createParagraph([textEditor.createInlineFromString("")]),
|
||||
textEditor.createParagraph([
|
||||
textEditor.createInlineFromString("¡Hola, Mundo!"),
|
||||
]),
|
||||
textEditor.createParagraph([
|
||||
textEditor.createInlineFromString("Hallo, Welt!"),
|
||||
]),
|
||||
]);
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
expect(textEditor.numParagraphs).toBe(4);
|
||||
});
|
||||
|
||||
test("Disposing a TextEditor nullifies everything", () => {
|
||||
const textEditor = new TextEditor(document.createElement("div"));
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
textEditor.dispose();
|
||||
expect(textEditor.root).toBe(null);
|
||||
expect(textEditor.element).toBe(null);
|
||||
});
|
||||
|
||||
test("TextEditor focus should focus the contenteditable element", () => {
|
||||
const textEditorElement = document.createElement("div");
|
||||
document.body.appendChild(textEditorElement);
|
||||
const textEditor = new TextEditor(textEditorElement);
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
textEditor.focus();
|
||||
expect(document.activeElement).toBe(textEditor.element);
|
||||
});
|
||||
|
||||
test("TextEditor blur should blur the contenteditable element", () => {
|
||||
const textEditorElement = document.createElement("div");
|
||||
document.body.appendChild(textEditorElement);
|
||||
const textEditor = new TextEditor(textEditorElement);
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
textEditor.focus();
|
||||
textEditor.blur();
|
||||
expect(document.activeElement).not.toBe(textEditor.element);
|
||||
});
|
||||
|
||||
test("TextEditor focus -> blur -> focus should restore old selection", () => {
|
||||
const textEditorElement = document.createElement("div");
|
||||
document.body.appendChild(textEditorElement);
|
||||
const textEditor = new TextEditor(textEditorElement);
|
||||
textEditor.root = textEditor.createRoot([
|
||||
textEditor.createParagraph([
|
||||
textEditor.createInlineFromString("Hello, World!"),
|
||||
]),
|
||||
]);
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
textEditor.focus();
|
||||
textEditor.blur();
|
||||
textEditor.focus();
|
||||
expect(document.activeElement).toBe(textEditor.element);
|
||||
});
|
||||
|
||||
test("TextEditor selectAll should select all the contenteditable", () => {
|
||||
const selection = document.getSelection();
|
||||
const textEditorElement = document.createElement("div");
|
||||
document.body.appendChild(textEditorElement);
|
||||
const textEditor = new TextEditor(textEditorElement);
|
||||
expect(textEditor).toBeInstanceOf(TextEditor);
|
||||
textEditor.focus();
|
||||
textEditor.selectAll();
|
||||
expect(document.activeElement).toBe(textEditor.element);
|
||||
expect(selection.containsNode(textEditor.root));
|
||||
});
|
||||
});
|
||||
19
frontend/text-editor/src/editor/clipboard/copy.js
Normal file
19
frontend/text-editor/src/editor/clipboard/copy.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* This event is called when the user copies a text from the
|
||||
* editor.
|
||||
*
|
||||
* TODO: We could transform `--fills` in here to CSS `color`, `background-image`,
|
||||
* etc. to be more compatible with other applications.
|
||||
*
|
||||
* @param {ClipboardEvent} event
|
||||
* @param {TextEditor} editor
|
||||
*/
|
||||
export function copy(event, editor) {}
|
||||
19
frontend/text-editor/src/editor/clipboard/cut.js
Normal file
19
frontend/text-editor/src/editor/clipboard/cut.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* This event is called when the user copies a text from the
|
||||
* editor.
|
||||
*
|
||||
* TODO: We could transform `--fills` in here to CSS `color`, `background-image`,
|
||||
* etc. to be more compatible with other applications.
|
||||
*
|
||||
* @param {ClipboardEvent} event
|
||||
* @param {TextEditor} editor
|
||||
*/
|
||||
export function cut(event, editor) {}
|
||||
17
frontend/text-editor/src/editor/clipboard/index.js
Normal file
17
frontend/text-editor/src/editor/clipboard/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { copy } from "./copy.js";
|
||||
import { cut } from "./cut.js";
|
||||
import { paste } from "./paste.js";
|
||||
|
||||
export default {
|
||||
copy,
|
||||
cut,
|
||||
paste,
|
||||
};
|
||||
45
frontend/text-editor/src/editor/clipboard/paste.js
Normal file
45
frontend/text-editor/src/editor/clipboard/paste.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { mapContentFragmentFromHTML, mapContentFragmentFromString } from "../content/dom/Content.js";
|
||||
|
||||
/**
|
||||
* When the user pastes some HTML, what we do is generate
|
||||
* a new DOM based on what the user pasted and then we
|
||||
* insert it in the appropiate part (see `insertFromPaste` command).
|
||||
*
|
||||
* @param {ClipboardEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
* @returns {void}
|
||||
*/
|
||||
export function paste(event, editor, selectionController) {
|
||||
// We need to prevent default behavior
|
||||
// because we don't allow any HTML to
|
||||
// be pasted.
|
||||
event.preventDefault();
|
||||
|
||||
let fragment = null;
|
||||
if (event.clipboardData.types.includes("text/html")) {
|
||||
const html = event.clipboardData.getData("text/html");
|
||||
fragment = mapContentFragmentFromHTML(html, selectionController.currentStyle);
|
||||
} else if (event.clipboardData.types.includes("text/plain")) {
|
||||
const plain = event.clipboardData.getData("text/plain");
|
||||
fragment = mapContentFragmentFromString(plain, selectionController.currentStyle);
|
||||
}
|
||||
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionController.isCollapsed) {
|
||||
selectionController.insertPaste(fragment);
|
||||
} else {
|
||||
selectionController.replaceWithPaste(fragment);
|
||||
}
|
||||
}
|
||||
66
frontend/text-editor/src/editor/commands/CommandMutations.js
Normal file
66
frontend/text-editor/src/editor/commands/CommandMutations.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Command mutations
|
||||
*/
|
||||
export class CommandMutations {
|
||||
#added = new Set();
|
||||
#removed = new Set();
|
||||
#updated = new Set();
|
||||
|
||||
constructor(added, updated, removed) {
|
||||
if (added && Array.isArray(added)) this.#added = new Set(added);
|
||||
if (updated && Array.isArray(updated)) this.#updated = new Set(updated);
|
||||
if (removed && Array.isArray(removed)) this.#removed = new Set(removed);
|
||||
}
|
||||
|
||||
get added() {
|
||||
return this.#added;
|
||||
}
|
||||
|
||||
get removed() {
|
||||
return this.#removed;
|
||||
}
|
||||
|
||||
get updated() {
|
||||
return this.#updated;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#added.clear();
|
||||
this.#removed.clear();
|
||||
this.#updated.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#added.clear();
|
||||
this.#added = null;
|
||||
this.#removed.clear();
|
||||
this.#removed = null;
|
||||
this.#updated.clear();
|
||||
this.#updated = null;
|
||||
}
|
||||
|
||||
add(node) {
|
||||
this.#added.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
remove(node) {
|
||||
this.#removed.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
update(node) {
|
||||
this.#updated.add(node);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommandMutations;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import CommandMutations from "./CommandMutations.js";
|
||||
|
||||
describe("CommandMutations", () => {
|
||||
test("should create a new CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
expect(mutations).toHaveProperty("added");
|
||||
expect(mutations).toHaveProperty("updated");
|
||||
expect(mutations).toHaveProperty("removed");
|
||||
});
|
||||
|
||||
test("should create an initialized new CommandMutations", () => {
|
||||
const mutations = new CommandMutations([1], [2], [3]);
|
||||
expect(mutations.added.size).toBe(1);
|
||||
expect(mutations.updated.size).toBe(1);
|
||||
expect(mutations.removed.size).toBe(1);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
expect(mutations.updated.has(2)).toBe(true);
|
||||
expect(mutations.removed.has(3)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an added node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an updated node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.update(1);
|
||||
expect(mutations.updated.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an removed node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.remove(1);
|
||||
expect(mutations.removed.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should clear a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
mutations.update(2);
|
||||
mutations.remove(3);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
expect(mutations.added.size).toBe(1);
|
||||
expect(mutations.updated.has(2)).toBe(true);
|
||||
expect(mutations.updated.size).toBe(1);
|
||||
expect(mutations.removed.has(3)).toBe(true);
|
||||
expect(mutations.removed.size).toBe(1);
|
||||
|
||||
mutations.clear();
|
||||
expect(mutations.added.size).toBe(0);
|
||||
expect(mutations.added.has(1)).toBe(false);
|
||||
expect(mutations.updated.size).toBe(0);
|
||||
expect(mutations.updated.has(1)).toBe(false);
|
||||
expect(mutations.removed.size).toBe(0);
|
||||
expect(mutations.removed.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
test("should dispose a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
mutations.update(2);
|
||||
mutations.remove(3);
|
||||
mutations.dispose();
|
||||
expect(mutations.added).toBe(null);
|
||||
expect(mutations.updated).toBe(null);
|
||||
expect(mutations.removed).toBe(null);
|
||||
});
|
||||
});
|
||||
22
frontend/text-editor/src/editor/commands/deleteByCut.js
Normal file
22
frontend/text-editor/src/editor/commands/deleteByCut.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Remove the current selection as part of a cut.
|
||||
*
|
||||
* @param {InputEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
*/
|
||||
export function deleteByCut(event, editor, selectionController) {
|
||||
event.preventDefault();
|
||||
if (selectionController.isCollapsed) {
|
||||
throw new Error("This should be impossible");
|
||||
}
|
||||
return selectionController.removeSelected();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* delete the content directly before the caret position and this intention is
|
||||
* not covered by another `inputType` or delete the selection with the
|
||||
* selection collapsing to its start after the deletion.
|
||||
*
|
||||
* @param {InputEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
*/
|
||||
export function deleteContentBackward(event, editor, selectionController) {
|
||||
event.preventDefault();
|
||||
// If the editor is empty this is a no op.
|
||||
if (editor.isEmpty) return;
|
||||
|
||||
// If not is collapsed AKA is a selection, then
|
||||
// we removeSelected.
|
||||
if (!selectionController.isCollapsed) {
|
||||
return selectionController.removeSelected({ direction: 'backward' });
|
||||
}
|
||||
|
||||
// If we're in a text node and the offset is
|
||||
// greater than 0 (not at the start of the inline)
|
||||
// we simple remove a character from the text.
|
||||
if (selectionController.isTextFocus && selectionController.focusOffset > 0) {
|
||||
return selectionController.removeBackwardText();
|
||||
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
} else if (
|
||||
selectionController.isTextFocus &&
|
||||
selectionController.focusAtStart
|
||||
) {
|
||||
return selectionController.mergeBackwardParagraph();
|
||||
|
||||
// If we're at an inline or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
} else if (
|
||||
selectionController.isInlineFocus ||
|
||||
selectionController.isLineBreakFocus
|
||||
) {
|
||||
return selectionController.removeBackwardParagraph();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* delete the content directly after the caret position and this intention is not covered by
|
||||
* another inputType or delete the selection with the selection collapsing to its end after the deletion
|
||||
*
|
||||
* @param {InputEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
*/
|
||||
export function deleteContentForward(event, editor, selectionController) {
|
||||
event.preventDefault();
|
||||
// If the editor is empty this is a no op.
|
||||
if (editor.isEmpty) return;
|
||||
|
||||
// If not is collapsed AKA is a selection, then
|
||||
// we removeSelected.
|
||||
if (!selectionController.isCollapsed) {
|
||||
return selectionController.removeSelected({ direction: "forward" });
|
||||
}
|
||||
|
||||
// If we're in a text node and the offset is
|
||||
// greater than 0 (not at the start of the inline)
|
||||
// we simple remove a character from the text.
|
||||
if (selectionController.isTextFocus
|
||||
&& selectionController.focusAtEnd) {
|
||||
return selectionController.mergeForwardParagraph();
|
||||
|
||||
// If we're in a text node but we're at the end of the
|
||||
// paragraph, we should merge the current paragraph
|
||||
// with the following paragraph.
|
||||
} else if (
|
||||
selectionController.isTextFocus &&
|
||||
selectionController.focusOffset >= 0
|
||||
) {
|
||||
return selectionController.removeForwardText();
|
||||
|
||||
// If we're at an inline or a line break paragraph
|
||||
// and there's more than one paragraph, then we should
|
||||
// remove the next paragraph.
|
||||
} else if (
|
||||
(selectionController.isInlineFocus ||
|
||||
selectionController.isLineBreakFocus) &&
|
||||
editor.numParagraphs > 1
|
||||
) {
|
||||
return selectionController.removeForwardParagraph();
|
||||
}
|
||||
}
|
||||
21
frontend/text-editor/src/editor/commands/index.js
Normal file
21
frontend/text-editor/src/editor/commands/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { insertText } from "./insertText.js";
|
||||
import { insertParagraph } from "./insertParagraph.js";
|
||||
import { deleteByCut } from "./deleteByCut.js";
|
||||
import { deleteContentBackward } from "./deleteContentBackward.js";
|
||||
import { deleteContentForward } from "./deleteContentForward.js";
|
||||
|
||||
export default {
|
||||
insertText,
|
||||
insertParagraph,
|
||||
deleteByCut,
|
||||
deleteContentBackward,
|
||||
deleteContentForward,
|
||||
};
|
||||
23
frontend/text-editor/src/editor/commands/insertParagraph.js
Normal file
23
frontend/text-editor/src/editor/commands/insertParagraph.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Insert a paragraph
|
||||
*
|
||||
* @see https://w3c.github.io/input-events/#interface-InputEvent
|
||||
* @param {InputEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
*/
|
||||
export function insertParagraph(event, editor, selectionController) {
|
||||
event.preventDefault();
|
||||
if (selectionController.isCollapsed) {
|
||||
return selectionController.insertParagraph();
|
||||
}
|
||||
return selectionController.replaceWithParagraph();
|
||||
}
|
||||
34
frontend/text-editor/src/editor/commands/insertText.js
Normal file
34
frontend/text-editor/src/editor/commands/insertText.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Insert typed plain text
|
||||
*
|
||||
* @see https://w3c.github.io/input-events/#interface-InputEvent
|
||||
* @param {InputEvent} event
|
||||
* @param {TextEditor} editor
|
||||
* @param {SelectionController} selectionController
|
||||
*/
|
||||
export function insertText(event, editor, selectionController) {
|
||||
event.preventDefault();
|
||||
if (selectionController.isCollapsed) {
|
||||
if (selectionController.isTextFocus) {
|
||||
return selectionController.insertText(event.data);
|
||||
} else if (selectionController.isLineBreakFocus) {
|
||||
return selectionController.replaceLineBreak(event.data);
|
||||
}
|
||||
} else {
|
||||
if (selectionController.isMultiParagraph) {
|
||||
return selectionController.replaceParagraphs(event.data);
|
||||
} else if (selectionController.isMultiInline) {
|
||||
return selectionController.replaceInlines(event.data);
|
||||
} else if (selectionController.isTextSame) {
|
||||
return selectionController.replaceText(event.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
frontend/text-editor/src/editor/content/Text.js
Normal file
104
frontend/text-editor/src/editor/content/Text.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throws if the passed value is not a valid offset value.
|
||||
*
|
||||
* @param {*} offset
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
function tryOffset(offset) {
|
||||
if (!Number.isInteger(offset) || offset < 0)
|
||||
throw new TypeError("Invalid offset");
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws if the passed value is not a valid string.
|
||||
*
|
||||
* @param {*} str
|
||||
* @throws {TypeError}
|
||||
*/
|
||||
function tryString(str) {
|
||||
if (typeof str !== "string") throw new TypeError("Invalid string");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts string into a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} offset
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function insertInto(str, offset, text) {
|
||||
tryString(str);
|
||||
tryOffset(offset);
|
||||
tryString(text);
|
||||
return str.slice(0, offset) + text + str.slice(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a part of a string with a string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} startOffset
|
||||
* @param {number} endOffset
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
export function replaceWith(str, startOffset, endOffset, text) {
|
||||
tryString(str);
|
||||
tryOffset(startOffset);
|
||||
tryOffset(endOffset);
|
||||
tryString(text);
|
||||
return str.slice(0, startOffset) + text + str.slice(endOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes text backward from specified offset.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} offset
|
||||
* @returns {string}
|
||||
*/
|
||||
export function removeBackward(str, offset) {
|
||||
tryString(str);
|
||||
tryOffset(offset);
|
||||
if (offset === 0) {
|
||||
return str;
|
||||
}
|
||||
return str.slice(0, offset - 1) + str.slice(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes text forward from specified offset.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} offset
|
||||
* @returns {string}
|
||||
*/
|
||||
export function removeForward(str, offset) {
|
||||
tryString(str);
|
||||
tryOffset(offset);
|
||||
return str.slice(0, offset) + str.slice(offset + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a slice of text.
|
||||
*
|
||||
* @param {string} str
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @returns {string}
|
||||
*/
|
||||
export function removeSlice(str, start, end) {
|
||||
tryString(str);
|
||||
tryOffset(start);
|
||||
tryOffset(end);
|
||||
return str.slice(0, start) + str.slice(end);
|
||||
}
|
||||
46
frontend/text-editor/src/editor/content/Text.test.js
Normal file
46
frontend/text-editor/src/editor/content/Text.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, test, expect } from 'vitest'
|
||||
import { insertInto, removeBackward, removeForward, replaceWith } from './Text';
|
||||
|
||||
describe("Text", () => {
|
||||
test("* should throw when passed wrong parameters", () => {
|
||||
expect(() => insertInto(Infinity, Infinity, Infinity)).toThrowError('Invalid string');
|
||||
expect(() => insertInto('Hello', Infinity, Infinity)).toThrowError('Invalid offset');
|
||||
expect(() => insertInto('Hello', 0, Infinity)).toThrowError('Invalid string');
|
||||
});
|
||||
|
||||
test("`insertInto` should insert a string into an offset", () => {
|
||||
expect(insertInto("Hell, World!", 4, "o")).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
test("`replaceWith` should replace a string into a string", () => {
|
||||
expect(replaceWith("Hello, Something!", 7, 16, "World")).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
test("`removeBackward` should remove string backward from start (offset 0)", () => {
|
||||
expect(removeBackward("Hello, World!", 0)).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
test("`removeForward` should remove string forward from start (offset 0)", () => {
|
||||
expect(removeForward("Hello, World!", 0)).toBe("ello, World!");
|
||||
});
|
||||
|
||||
test("`removeBackward` should remove string backward from end", () => {
|
||||
expect(removeBackward("Hello, World!", "Hello, World!".length)).toBe(
|
||||
"Hello, World"
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeForward` should remove string forward from end", () => {
|
||||
expect(removeForward("Hello, World!", "Hello, World!".length)).toBe(
|
||||
"Hello, World!"
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeBackward` should remove string backward from offset 6", () => {
|
||||
expect(removeBackward("Hello, World!", 6)).toBe("Hello World!");
|
||||
});
|
||||
|
||||
test("`removeForward` should remove string forward from offset 6", () => {
|
||||
expect(removeForward("Hello, World!", 6)).toBe("Hello,World!");
|
||||
});
|
||||
});
|
||||
78
frontend/text-editor/src/editor/content/dom/Color.js
Normal file
78
frontend/text-editor/src/editor/content/dom/Color.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Canvas used to retrieve colors as CSS hexadecimals.
|
||||
*
|
||||
* @type {OffscreenCanvas|HTMLCanvasElement}
|
||||
*/
|
||||
let canvas = null; // createCanvas(1, 1);
|
||||
|
||||
/**
|
||||
* Context used to retrieve colors as CSS hexadecimals.
|
||||
*
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
let context = null; // canvas.getContext("2d");
|
||||
|
||||
/**
|
||||
* Returns the canvas context.
|
||||
*
|
||||
* @returns {CanvasRenderingContext2D}
|
||||
*/
|
||||
function getContext() {
|
||||
if (!canvas) {
|
||||
canvas = createCanvas(1, 1);
|
||||
}
|
||||
if (!context) {
|
||||
context = canvas.getContext("2d");
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new canvas element.
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @returns {OffscreenCanvas|HTMLCanvasElement}
|
||||
*/
|
||||
function createCanvas(width, height) {
|
||||
if ("OffscreenCanvas" in globalThis) {
|
||||
return new OffscreenCanvas(width, height);
|
||||
}
|
||||
return document.createElement("canvas");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte representation as an hex.
|
||||
*
|
||||
* @param {number} byte
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getByteAsHex(byte) {
|
||||
return byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a color definition from a fillStyle color.
|
||||
*
|
||||
* @param {string} fillStyle
|
||||
* @returns {[string, number]}
|
||||
*/
|
||||
export function getColor(fillStyle) {
|
||||
const context = getContext();
|
||||
context.fillStyle = fillStyle;
|
||||
context.fillRect(0, 0, 1, 1);
|
||||
const imageData = context.getImageData(0, 0, 1, 1);
|
||||
const [r, g, b, a] = imageData.data;
|
||||
return [`#${getByteAsHex(r)}${getByteAsHex(g)}${getByteAsHex(b)}`, a / 255.0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a fill from a fillStyle color.
|
||||
*
|
||||
* @param {string} fillStyle
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFills(fillStyle) {
|
||||
const [color, opacity] = getColor(fillStyle);
|
||||
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
|
||||
}
|
||||
102
frontend/text-editor/src/editor/content/dom/Content.js
Normal file
102
frontend/text-editor/src/editor/content/dom/Content.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { createInline } from "./Inline.js";
|
||||
import {
|
||||
createEmptyParagraph,
|
||||
createParagraph,
|
||||
isLikeParagraph,
|
||||
} from "./Paragraph.js";
|
||||
import { isDisplayBlock, normalizeStyles } from "./Style.js";
|
||||
|
||||
/**
|
||||
* Maps any HTML into a valid content DOM element.
|
||||
*
|
||||
* @param {Document} document
|
||||
* @param {HTMLElement} root
|
||||
* @param {CSSStyleDeclaration} [styleDefaults]
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
export function mapContentFragmentFromDocument(document, root, styleDefaults) {
|
||||
const nodeIterator = document.createNodeIterator(
|
||||
root,
|
||||
NodeFilter.SHOW_TEXT
|
||||
);
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
let currentParagraph = null;
|
||||
let currentNode = nodeIterator.nextNode();
|
||||
while (currentNode) {
|
||||
// We cannot call document.defaultView because it is `null`.
|
||||
const currentStyle = normalizeStyles(currentNode, styleDefaults);
|
||||
if (
|
||||
isDisplayBlock(currentNode.parentElement.style) ||
|
||||
isDisplayBlock(currentStyle) ||
|
||||
isLikeParagraph(currentNode.parentElement)
|
||||
) {
|
||||
if (currentParagraph) {
|
||||
fragment.appendChild(currentParagraph);
|
||||
}
|
||||
currentParagraph = createParagraph(undefined, currentStyle);
|
||||
} else {
|
||||
if (currentParagraph === null) {
|
||||
currentParagraph = createParagraph(undefined, currentStyle);
|
||||
}
|
||||
}
|
||||
|
||||
currentParagraph.appendChild(
|
||||
createInline(new Text(currentNode.nodeValue), currentStyle)
|
||||
);
|
||||
|
||||
currentNode = nodeIterator.nextNode();
|
||||
}
|
||||
|
||||
fragment.appendChild(currentParagraph);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps any HTML into a valid content DOM element.
|
||||
*
|
||||
* @param {string} html
|
||||
* @param {CSSStyleDeclaration} [styleDefaults]
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
export function mapContentFragmentFromHTML(html, styleDefaults) {
|
||||
const parser = new DOMParser();
|
||||
const htmlDocument = parser.parseFromString(html, "text/html");
|
||||
return mapContentFragmentFromDocument(
|
||||
htmlDocument,
|
||||
htmlDocument.documentElement,
|
||||
styleDefaults
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a plain text into a valid content DOM element.
|
||||
*
|
||||
* @param {string} string
|
||||
* @param {CSSStyleDeclaration} [styleDefaults]
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
export function mapContentFragmentFromString(string, styleDefaults) {
|
||||
const lines = string.replace(/\r/g, "").split("\n");
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const line of lines) {
|
||||
if (line === "") {
|
||||
fragment.appendChild(createEmptyParagraph(styleDefaults));
|
||||
} else {
|
||||
fragment.appendChild(
|
||||
createParagraph([
|
||||
createInline(new Text(line), styleDefaults)
|
||||
], styleDefaults)
|
||||
);
|
||||
}
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user