@rogieking/figui3 3.22.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/components.css +87 -21
- package/fig.js +486 -186
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1001,10 +1001,9 @@ An image display component with optional upload, aspect ratio, and object-fit co
|
|
|
1001
1001
|
| Attribute | Type | Default | Description |
|
|
1002
1002
|
|---|---|---|---|
|
|
1003
1003
|
| `src` | string | — | Image URL |
|
|
1004
|
-
| `upload` | boolean | `false` | Show upload
|
|
1005
|
-
| `
|
|
1006
|
-
| `
|
|
1007
|
-
| `aspect-ratio` | string | — | CSS aspect-ratio (e.g. `"16 / 9"`) |
|
|
1004
|
+
| `upload` | boolean | `false` | Show upload overlay (`fig-input-file`) |
|
|
1005
|
+
| `label` | string | `"Upload"` | Upload button label |
|
|
1006
|
+
| `aspect-ratio` | string | — | CSS aspect-ratio, or `"auto"` for lazy dimension detection |
|
|
1008
1007
|
| `fit` | string | — | CSS object-fit (`"cover"`, `"contain"`, etc.) |
|
|
1009
1008
|
| `checkerboard` | boolean | `false` | Show checkerboard behind transparent images |
|
|
1010
1009
|
|
package/components.css
CHANGED
|
@@ -1290,6 +1290,87 @@ fig-chit {
|
|
|
1290
1290
|
}
|
|
1291
1291
|
|
|
1292
1292
|
/* Fig Image */
|
|
1293
|
+
fig-truncate {
|
|
1294
|
+
display: inline-block;
|
|
1295
|
+
white-space: nowrap;
|
|
1296
|
+
overflow: hidden;
|
|
1297
|
+
text-overflow: ellipsis;
|
|
1298
|
+
min-width: 0;
|
|
1299
|
+
max-width: 100%;
|
|
1300
|
+
vertical-align: bottom;
|
|
1301
|
+
|
|
1302
|
+
&[position="left"] {
|
|
1303
|
+
display: block;
|
|
1304
|
+
direction: rtl;
|
|
1305
|
+
text-align: left;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
&[position="middle"] {
|
|
1309
|
+
display: inline-flex;
|
|
1310
|
+
text-overflow: clip;
|
|
1311
|
+
gap: 0;
|
|
1312
|
+
|
|
1313
|
+
& > .start,
|
|
1314
|
+
& > .end {
|
|
1315
|
+
flex: 0 1 auto;
|
|
1316
|
+
white-space: nowrap;
|
|
1317
|
+
overflow: hidden;
|
|
1318
|
+
text-overflow: ellipsis;
|
|
1319
|
+
min-width: 0;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
& > .end {
|
|
1323
|
+
direction: rtl; /* technique #2 on right half */
|
|
1324
|
+
text-align: left;
|
|
1325
|
+
text-overflow: ellipsis;
|
|
1326
|
+
}
|
|
1327
|
+
&[tail] {
|
|
1328
|
+
& > .end {
|
|
1329
|
+
direction: ltr;
|
|
1330
|
+
text-align: right;
|
|
1331
|
+
flex-shrink: 0;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
fig-input-file {
|
|
1338
|
+
display: flex;
|
|
1339
|
+
align-items: center;
|
|
1340
|
+
gap: var(--spacer-2);
|
|
1341
|
+
min-width: 0;
|
|
1342
|
+
|
|
1343
|
+
&[full]:not([full="false"]) {
|
|
1344
|
+
flex: 1 1 auto;
|
|
1345
|
+
width: 100%;
|
|
1346
|
+
|
|
1347
|
+
& > fig-tooltip,
|
|
1348
|
+
& > fig-button {
|
|
1349
|
+
flex: 1;
|
|
1350
|
+
min-width: 0;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
& > fig-tooltip > fig-button {
|
|
1354
|
+
width: 100%;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
.fig-input-file-filename {
|
|
1359
|
+
flex: 1;
|
|
1360
|
+
min-width: 0;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.fig-input-file-clear {
|
|
1364
|
+
flex-shrink: 0;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
&[dragover] {
|
|
1368
|
+
outline: 2px dashed var(--figma-color-border-brand);
|
|
1369
|
+
outline-offset: -2px;
|
|
1370
|
+
border-radius: var(--radius-small);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1293
1374
|
fig-image {
|
|
1294
1375
|
--image-size: 2rem;
|
|
1295
1376
|
--fit: cover;
|
|
@@ -1314,8 +1395,12 @@ fig-image {
|
|
|
1314
1395
|
opacity: 1;
|
|
1315
1396
|
}
|
|
1316
1397
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1398
|
+
> fig-input-file[data-generated] {
|
|
1399
|
+
opacity: 0;
|
|
1400
|
+
pointer-events: none;
|
|
1401
|
+
}
|
|
1402
|
+
&:not([src]):not([src=""]) > fig-input-file[data-generated],
|
|
1403
|
+
&:hover > fig-input-file[data-generated] {
|
|
1319
1404
|
opacity: 1;
|
|
1320
1405
|
pointer-events: all;
|
|
1321
1406
|
}
|
|
@@ -1339,25 +1424,6 @@ fig-image {
|
|
|
1339
1424
|
aspect-ratio: var(--aspect-ratio) !important;
|
|
1340
1425
|
}
|
|
1341
1426
|
}
|
|
1342
|
-
> div {
|
|
1343
|
-
display: flex;
|
|
1344
|
-
gap: var(--spacer-2);
|
|
1345
|
-
opacity: 0;
|
|
1346
|
-
pointer-events: none;
|
|
1347
|
-
}
|
|
1348
|
-
&:not([src]):not([src=""]) > div,
|
|
1349
|
-
&:hover > div {
|
|
1350
|
-
opacity: 1;
|
|
1351
|
-
pointer-events: all;
|
|
1352
|
-
}
|
|
1353
|
-
fig-button[type="download"] {
|
|
1354
|
-
display: none;
|
|
1355
|
-
}
|
|
1356
|
-
&[src]:not([src=""]) {
|
|
1357
|
-
fig-button[type="download"] {
|
|
1358
|
-
display: inline-flex;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
1427
|
}
|
|
1362
1428
|
|
|
1363
1429
|
/* Easing Curve */
|
package/fig.js
CHANGED
|
@@ -1023,6 +1023,85 @@ class FigTooltip extends HTMLElement {
|
|
|
1023
1023
|
|
|
1024
1024
|
customElements.define("fig-tooltip", FigTooltip);
|
|
1025
1025
|
|
|
1026
|
+
/* Text Truncation */
|
|
1027
|
+
class FigTruncate extends HTMLElement {
|
|
1028
|
+
static observedAttributes = ["position", "tail"];
|
|
1029
|
+
|
|
1030
|
+
#originalText = null;
|
|
1031
|
+
#boundEnter = null;
|
|
1032
|
+
#boundLeave = null;
|
|
1033
|
+
|
|
1034
|
+
connectedCallback() {
|
|
1035
|
+
this.#originalText = this.textContent;
|
|
1036
|
+
requestAnimationFrame(() => {
|
|
1037
|
+
this.#render();
|
|
1038
|
+
this.#setupTooltip();
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
disconnectedCallback() {
|
|
1043
|
+
this.#teardownTooltip();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1047
|
+
if (oldValue === newValue) return;
|
|
1048
|
+
if (this.#originalText === null) return;
|
|
1049
|
+
this.#render();
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
#render() {
|
|
1053
|
+
const position = this.getAttribute("position") || "right";
|
|
1054
|
+
const text = this.#originalText || "";
|
|
1055
|
+
if (position === "middle") {
|
|
1056
|
+
const tail = this.getAttribute("tail");
|
|
1057
|
+
let splitIndex;
|
|
1058
|
+
if (tail) {
|
|
1059
|
+
const idx = text.lastIndexOf(tail);
|
|
1060
|
+
splitIndex = idx > 0 ? idx : Math.ceil(text.length / 2);
|
|
1061
|
+
} else {
|
|
1062
|
+
splitIndex = Math.ceil(text.length / 2);
|
|
1063
|
+
}
|
|
1064
|
+
this.innerHTML = "";
|
|
1065
|
+
const startSpan = document.createElement("span");
|
|
1066
|
+
startSpan.className = "start";
|
|
1067
|
+
startSpan.textContent = text.slice(0, splitIndex);
|
|
1068
|
+
const endSpan = document.createElement("span");
|
|
1069
|
+
endSpan.className = "end";
|
|
1070
|
+
endSpan.textContent = text.slice(splitIndex);
|
|
1071
|
+
this.appendChild(startSpan);
|
|
1072
|
+
this.appendChild(endSpan);
|
|
1073
|
+
} else {
|
|
1074
|
+
this.textContent = text;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
#setupTooltip() {
|
|
1079
|
+
if (
|
|
1080
|
+
!this.hasAttribute("tooltip") ||
|
|
1081
|
+
this.getAttribute("tooltip") === "false"
|
|
1082
|
+
)
|
|
1083
|
+
return;
|
|
1084
|
+
this.#boundEnter = () => {
|
|
1085
|
+
if (this.scrollWidth <= this.clientWidth) return;
|
|
1086
|
+
FigTooltip.show(this, this.#originalText);
|
|
1087
|
+
};
|
|
1088
|
+
this.#boundLeave = () => {
|
|
1089
|
+
FigTooltip.hide(this);
|
|
1090
|
+
};
|
|
1091
|
+
this.addEventListener("pointerenter", this.#boundEnter);
|
|
1092
|
+
this.addEventListener("pointerleave", this.#boundLeave);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
#teardownTooltip() {
|
|
1096
|
+
if (this.#boundEnter)
|
|
1097
|
+
this.removeEventListener("pointerenter", this.#boundEnter);
|
|
1098
|
+
if (this.#boundLeave)
|
|
1099
|
+
this.removeEventListener("pointerleave", this.#boundLeave);
|
|
1100
|
+
FigTooltip.hide(this);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
customElements.define("fig-truncate", FigTruncate);
|
|
1104
|
+
|
|
1026
1105
|
/* Dialog */
|
|
1027
1106
|
/**
|
|
1028
1107
|
* A custom dialog element for modal and non-modal dialogs.
|
|
@@ -7928,57 +8007,58 @@ customElements.define("fig-chit", FigChit);
|
|
|
7928
8007
|
class FigSwatch extends FigChit {}
|
|
7929
8008
|
customElements.define("fig-swatch", FigSwatch);
|
|
7930
8009
|
|
|
7931
|
-
/*
|
|
8010
|
+
/* Image */
|
|
7932
8011
|
/**
|
|
7933
|
-
*
|
|
7934
|
-
* @attr {
|
|
7935
|
-
* @attr {
|
|
7936
|
-
* @attr {string}
|
|
7937
|
-
* @attr {string}
|
|
8012
|
+
* @attr {string} src - Image source URL
|
|
8013
|
+
* @attr {boolean} upload - Show upload overlay (generates fig-input-file)
|
|
8014
|
+
* @attr {string} label - Upload button label (default "Upload")
|
|
8015
|
+
* @attr {string} size - small | medium | large | auto
|
|
8016
|
+
* @attr {string} aspect-ratio - CSS aspect-ratio or "auto" (lazy dimension sniff)
|
|
8017
|
+
* @attr {string} fit - CSS object-fit value
|
|
8018
|
+
* @attr {boolean} checkerboard - Show checkerboard behind transparent images
|
|
7938
8019
|
*/
|
|
7939
8020
|
class FigImage extends HTMLElement {
|
|
7940
8021
|
#src = null;
|
|
8022
|
+
#chit = null;
|
|
8023
|
+
#fileInput = null;
|
|
8024
|
+
#blobUrl = null;
|
|
8025
|
+
#file = null;
|
|
7941
8026
|
#boundHandleFileInput = this.#handleFileInput.bind(this);
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
8027
|
+
|
|
8028
|
+
static get observedAttributes() {
|
|
8029
|
+
return ["src", "upload", "aspect-ratio", "fit", "checkerboard"];
|
|
7945
8030
|
}
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
this.getAttribute("checkerboard") !== "false";
|
|
7950
|
-
const bg = this.src
|
|
7951
|
-
? `url(${this.src})`
|
|
7952
|
-
: cb
|
|
7953
|
-
? "url()"
|
|
7954
|
-
: "var(--figma-color-bg-secondary)";
|
|
7955
|
-
return `<fig-chit size="large" data-type="image" background="${bg}" disabled${cb ? " checkerboard" : ""}></fig-chit><div>${
|
|
7956
|
-
this.upload
|
|
7957
|
-
? `<fig-button variant="overlay" type="upload">
|
|
7958
|
-
${this.label}
|
|
7959
|
-
<input type="file" accept="image/*" />
|
|
7960
|
-
</fig-button>`
|
|
7961
|
-
: ""
|
|
7962
|
-
} ${
|
|
7963
|
-
this.download
|
|
7964
|
-
? `<fig-button variant="overlay" icon="true" type="download">
|
|
7965
|
-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
7966
|
-
<path d="M17.5 13C17.7761 13 18 13.2239 18 13.5V16.5C18 17.3284 17.3284 18 16.5 18H7.5C6.67157 18 6 17.3284 6 16.5V13.5C6 13.2239 6.22386 13 6.5 13C6.77614 13 7 13.2239 7 13.5V16.5C7 16.7761 7.22386 17 7.5 17H16.5C16.7761 17 17 16.7761 17 16.5V13.5C17 13.2239 17.2239 13 17.5 13ZM12 6C12.2761 6 12.5 6.22386 12.5 6.5V12.293L14.6465 10.1465C14.8417 9.95122 15.1583 9.95122 15.3535 10.1465C15.5488 10.3417 15.5488 10.6583 15.3535 10.8535L12.3535 13.8535C12.2597 13.9473 12.1326 14 12 14C11.9006 14 11.8042 13.9704 11.7227 13.916L11.6465 13.8535L8.64648 10.8535C8.45122 10.6583 8.45122 10.3417 8.64648 10.1465C8.84175 9.95122 9.15825 9.95122 9.35352 10.1465L11.5 12.293V6.5C11.5 6.22386 11.7239 6 12 6Z" fill="black"/>
|
|
7967
|
-
</svg></fig-button>`
|
|
7968
|
-
: ""
|
|
7969
|
-
}</div>`;
|
|
8031
|
+
|
|
8032
|
+
get src() {
|
|
8033
|
+
return this.#src;
|
|
7970
8034
|
}
|
|
8035
|
+
set src(value) {
|
|
8036
|
+
this.#src = value;
|
|
8037
|
+
this.setAttribute("src", value);
|
|
8038
|
+
}
|
|
8039
|
+
|
|
8040
|
+
get file() {
|
|
8041
|
+
return this.#file;
|
|
8042
|
+
}
|
|
8043
|
+
|
|
8044
|
+
async getBase64() {
|
|
8045
|
+
const src = this.#src;
|
|
8046
|
+
if (!src) return null;
|
|
8047
|
+
const res = await fetch(src);
|
|
8048
|
+
const blob = await res.blob();
|
|
8049
|
+
const bitmap = await createImageBitmap(blob);
|
|
8050
|
+
const canvas = document.createElement("canvas");
|
|
8051
|
+
canvas.width = bitmap.width;
|
|
8052
|
+
canvas.height = bitmap.height;
|
|
8053
|
+
canvas.getContext("2d").drawImage(bitmap, 0, 0);
|
|
8054
|
+
bitmap.close();
|
|
8055
|
+
const dataUrl = canvas.toDataURL();
|
|
8056
|
+
return dataUrl;
|
|
8057
|
+
}
|
|
8058
|
+
|
|
7971
8059
|
connectedCallback() {
|
|
7972
8060
|
this.#src = this.getAttribute("src") || "";
|
|
7973
|
-
|
|
7974
|
-
this.hasAttribute("upload") && this.getAttribute("upload") !== "false";
|
|
7975
|
-
this.download =
|
|
7976
|
-
this.hasAttribute("download") &&
|
|
7977
|
-
this.getAttribute("download") !== "false";
|
|
7978
|
-
this.label = this.getAttribute("label") || "Upload";
|
|
7979
|
-
this.size = this.getAttribute("size") || "small";
|
|
7980
|
-
this.innerHTML = this.#getInnerHTML();
|
|
7981
|
-
this.#updateRefs();
|
|
8061
|
+
|
|
7982
8062
|
const ar = this.getAttribute("aspect-ratio");
|
|
7983
8063
|
if (ar && ar !== "auto") {
|
|
7984
8064
|
this.style.setProperty("--aspect-ratio", ar);
|
|
@@ -7987,182 +8067,169 @@ class FigImage extends HTMLElement {
|
|
|
7987
8067
|
if (fit) {
|
|
7988
8068
|
this.style.setProperty("--fit", fit);
|
|
7989
8069
|
}
|
|
8070
|
+
|
|
8071
|
+
if (!this.querySelector("fig-chit")) {
|
|
8072
|
+
const chit = document.createElement("fig-chit");
|
|
8073
|
+
chit.setAttribute("data-generated", "");
|
|
8074
|
+
chit.setAttribute("size", "large");
|
|
8075
|
+
chit.setAttribute("data-type", "image");
|
|
8076
|
+
chit.setAttribute("disabled", "");
|
|
8077
|
+
this.#applyChitBackground(chit);
|
|
8078
|
+
if (this.hasAttribute("checkerboard") && this.getAttribute("checkerboard") !== "false") {
|
|
8079
|
+
chit.setAttribute("checkerboard", "");
|
|
8080
|
+
}
|
|
8081
|
+
this.prepend(chit);
|
|
8082
|
+
}
|
|
8083
|
+
this.#chit = this.querySelector("fig-chit");
|
|
8084
|
+
|
|
8085
|
+
const isUpload = this.hasAttribute("upload") && this.getAttribute("upload") !== "false";
|
|
8086
|
+
if (isUpload && !this.querySelector("fig-input-file[data-generated]")) {
|
|
8087
|
+
this.#createFileInput();
|
|
8088
|
+
}
|
|
8089
|
+
|
|
8090
|
+
if (this.#src && this.getAttribute("aspect-ratio") === "auto") {
|
|
8091
|
+
this.#sniffDimensions(this.#src);
|
|
8092
|
+
}
|
|
7990
8093
|
}
|
|
8094
|
+
|
|
7991
8095
|
disconnectedCallback() {
|
|
7992
|
-
this
|
|
7993
|
-
this
|
|
7994
|
-
|
|
7995
|
-
this.#
|
|
7996
|
-
|
|
8096
|
+
this.#fileInput?.removeEventListener("change", this.#boundHandleFileInput);
|
|
8097
|
+
if (this.#blobUrl) {
|
|
8098
|
+
URL.revokeObjectURL(this.#blobUrl);
|
|
8099
|
+
this.#blobUrl = null;
|
|
8100
|
+
}
|
|
7997
8101
|
}
|
|
7998
8102
|
|
|
7999
|
-
#
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
"change",
|
|
8007
|
-
this.#boundHandleFileInput,
|
|
8008
|
-
);
|
|
8009
|
-
this.fileInput?.addEventListener("change", this.#boundHandleFileInput);
|
|
8010
|
-
}
|
|
8011
|
-
if (this.download) {
|
|
8012
|
-
this.downloadButton = this.querySelector("fig-button[type='download']");
|
|
8013
|
-
this.downloadButton?.removeEventListener(
|
|
8014
|
-
"click",
|
|
8015
|
-
this.#boundHandleDownload,
|
|
8016
|
-
);
|
|
8017
|
-
this.downloadButton?.addEventListener(
|
|
8018
|
-
"click",
|
|
8019
|
-
this.#boundHandleDownload,
|
|
8020
|
-
);
|
|
8021
|
-
}
|
|
8022
|
-
});
|
|
8103
|
+
#applyChitBackground(chit) {
|
|
8104
|
+
const cb = this.hasAttribute("checkerboard") && this.getAttribute("checkerboard") !== "false";
|
|
8105
|
+
if (this.#src) {
|
|
8106
|
+
chit.setAttribute("background", `url(${this.#src})`);
|
|
8107
|
+
} else {
|
|
8108
|
+
chit.setAttribute("background", cb ? "url()" : "var(--figma-color-bg-secondary)");
|
|
8109
|
+
}
|
|
8023
8110
|
}
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
const
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
this.image.onload = async () => {
|
|
8037
|
-
this.aspectRatio = this.image.width / this.image.height;
|
|
8038
|
-
const ar = this.getAttribute("aspect-ratio");
|
|
8039
|
-
if (!ar || ar === "auto") {
|
|
8040
|
-
this.style.setProperty(
|
|
8041
|
-
"--aspect-ratio",
|
|
8042
|
-
`${this.image.width}/${this.image.height}`,
|
|
8043
|
-
);
|
|
8044
|
-
}
|
|
8045
|
-
this.dispatchEvent(
|
|
8046
|
-
new CustomEvent("loaded", {
|
|
8047
|
-
bubbles: true,
|
|
8048
|
-
cancelable: true,
|
|
8049
|
-
detail: {
|
|
8050
|
-
blob: this.blob,
|
|
8051
|
-
base64: this.base64,
|
|
8052
|
-
},
|
|
8053
|
-
}),
|
|
8054
|
-
);
|
|
8055
|
-
resolve();
|
|
8056
|
-
|
|
8057
|
-
// Create canvas to extract blob and base64 from image
|
|
8058
|
-
const canvas = document.createElement("canvas");
|
|
8059
|
-
const ctx = canvas.getContext("2d");
|
|
8060
|
-
canvas.width = this.image.width;
|
|
8061
|
-
canvas.height = this.image.height;
|
|
8062
|
-
ctx.drawImage(this.image, 0, 0);
|
|
8063
|
-
|
|
8064
|
-
// Get base64 from canvas
|
|
8065
|
-
this.base64 = canvas.toDataURL();
|
|
8066
|
-
|
|
8067
|
-
// Get blob from canvas
|
|
8068
|
-
canvas.toBlob((blob) => {
|
|
8069
|
-
if (this.blob) {
|
|
8070
|
-
URL.revokeObjectURL(this.blob);
|
|
8071
|
-
}
|
|
8072
|
-
if (blob) {
|
|
8073
|
-
this.blob = URL.createObjectURL(blob);
|
|
8074
|
-
}
|
|
8075
|
-
});
|
|
8076
|
-
};
|
|
8077
|
-
this.image.src = src;
|
|
8078
|
-
});
|
|
8111
|
+
|
|
8112
|
+
#createFileInput() {
|
|
8113
|
+
const fi = document.createElement("fig-input-file");
|
|
8114
|
+
fi.setAttribute("data-generated", "");
|
|
8115
|
+
fi.setAttribute("accepts", "image/*");
|
|
8116
|
+
fi.setAttribute("variant", "overlay");
|
|
8117
|
+
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8118
|
+
fi.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8119
|
+
if (this.#src) fi.setAttribute("url", this.#src);
|
|
8120
|
+
fi.addEventListener("change", this.#boundHandleFileInput);
|
|
8121
|
+
this.append(fi);
|
|
8122
|
+
this.#fileInput = fi;
|
|
8079
8123
|
}
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8124
|
+
|
|
8125
|
+
#removeFileInput() {
|
|
8126
|
+
if (this.#fileInput) {
|
|
8127
|
+
this.#fileInput.removeEventListener("change", this.#boundHandleFileInput);
|
|
8128
|
+
this.#fileInput.remove();
|
|
8129
|
+
this.#fileInput = null;
|
|
8130
|
+
}
|
|
8131
|
+
}
|
|
8132
|
+
|
|
8133
|
+
#handleFileInput(e) {
|
|
8134
|
+
const file = e.detail?.files?.[0];
|
|
8135
|
+
|
|
8136
|
+
if (!file) {
|
|
8137
|
+
if (this.#blobUrl) {
|
|
8138
|
+
URL.revokeObjectURL(this.#blobUrl);
|
|
8139
|
+
this.#blobUrl = null;
|
|
8140
|
+
}
|
|
8141
|
+
this.#file = null;
|
|
8142
|
+
this.removeAttribute("src");
|
|
8143
|
+
this.dispatchEvent(
|
|
8144
|
+
new CustomEvent("change", { bubbles: true, cancelable: true }),
|
|
8145
|
+
);
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
8148
|
+
|
|
8149
|
+
if (this.#blobUrl) {
|
|
8150
|
+
URL.revokeObjectURL(this.#blobUrl);
|
|
8151
|
+
}
|
|
8152
|
+
this.#file = file;
|
|
8153
|
+
this.#blobUrl = URL.createObjectURL(file);
|
|
8154
|
+
|
|
8155
|
+
this.setAttribute("src", this.#blobUrl);
|
|
8156
|
+
|
|
8096
8157
|
this.dispatchEvent(
|
|
8097
8158
|
new CustomEvent("loaded", {
|
|
8098
8159
|
bubbles: true,
|
|
8099
8160
|
cancelable: true,
|
|
8100
|
-
detail: {
|
|
8101
|
-
blob: this.blob,
|
|
8102
|
-
base64: this.base64,
|
|
8103
|
-
},
|
|
8161
|
+
detail: { file, src: this.#blobUrl },
|
|
8104
8162
|
}),
|
|
8105
8163
|
);
|
|
8106
|
-
//emit for change too
|
|
8107
8164
|
this.dispatchEvent(
|
|
8108
|
-
new CustomEvent("change", {
|
|
8109
|
-
bubbles: true,
|
|
8110
|
-
cancelable: true,
|
|
8111
|
-
}),
|
|
8165
|
+
new CustomEvent("change", { bubbles: true, cancelable: true }),
|
|
8112
8166
|
);
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
get src() {
|
|
8119
|
-
return this.#src;
|
|
8167
|
+
|
|
8168
|
+
if (this.#fileInput) {
|
|
8169
|
+
this.#fileInput.clear();
|
|
8170
|
+
this.#fileInput.setAttribute("label", "Replace");
|
|
8171
|
+
}
|
|
8120
8172
|
}
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8173
|
+
|
|
8174
|
+
async #sniffDimensions(src) {
|
|
8175
|
+
try {
|
|
8176
|
+
let blob;
|
|
8177
|
+
if (src.startsWith("blob:")) {
|
|
8178
|
+
const res = await fetch(src);
|
|
8179
|
+
blob = await res.blob();
|
|
8180
|
+
} else {
|
|
8181
|
+
const res = await fetch(src, { mode: "cors" });
|
|
8182
|
+
blob = await res.blob();
|
|
8183
|
+
}
|
|
8184
|
+
const bitmap = await createImageBitmap(blob);
|
|
8185
|
+
this.style.setProperty("--aspect-ratio", `${bitmap.width}/${bitmap.height}`);
|
|
8186
|
+
bitmap.close();
|
|
8187
|
+
} catch {
|
|
8188
|
+
// Non-critical — CSS aspect-ratio fallback handles it
|
|
8189
|
+
}
|
|
8124
8190
|
}
|
|
8125
8191
|
|
|
8126
8192
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
8193
|
+
if (oldValue === newValue) return;
|
|
8194
|
+
|
|
8127
8195
|
if (name === "src") {
|
|
8128
8196
|
this.#src = newValue;
|
|
8129
|
-
if (this
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8197
|
+
if (this.#chit) {
|
|
8198
|
+
this.#applyChitBackground(this.#chit);
|
|
8199
|
+
}
|
|
8200
|
+
if (this.#fileInput) {
|
|
8201
|
+
const defaultLabel = this.getAttribute("label") || "Upload";
|
|
8202
|
+
this.#fileInput.setAttribute("label", this.#src ? "Replace" : defaultLabel);
|
|
8133
8203
|
if (this.#src) {
|
|
8134
|
-
this.
|
|
8204
|
+
this.#fileInput.setAttribute("url", this.#src);
|
|
8135
8205
|
} else {
|
|
8136
|
-
this.
|
|
8137
|
-
"background",
|
|
8138
|
-
hasCb ? "url()" : "var(--figma-color-bg-secondary)",
|
|
8139
|
-
);
|
|
8206
|
+
this.#fileInput.removeAttribute("url");
|
|
8140
8207
|
}
|
|
8141
8208
|
}
|
|
8142
|
-
if (this.#src) {
|
|
8143
|
-
this.#
|
|
8209
|
+
if (this.#src && this.getAttribute("aspect-ratio") === "auto") {
|
|
8210
|
+
this.#sniffDimensions(this.#src);
|
|
8144
8211
|
}
|
|
8145
8212
|
}
|
|
8213
|
+
|
|
8146
8214
|
if (name === "upload") {
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
this.innerHTML = this.#getInnerHTML();
|
|
8154
|
-
this.#updateRefs();
|
|
8155
|
-
}
|
|
8156
|
-
if (name === "size") {
|
|
8157
|
-
this.size = newValue;
|
|
8215
|
+
const on = newValue !== null && newValue !== "false";
|
|
8216
|
+
if (on && !this.#fileInput) {
|
|
8217
|
+
this.#createFileInput();
|
|
8218
|
+
} else if (!on) {
|
|
8219
|
+
this.#removeFileInput();
|
|
8220
|
+
}
|
|
8158
8221
|
}
|
|
8222
|
+
|
|
8159
8223
|
if (name === "aspect-ratio") {
|
|
8160
8224
|
if (newValue && newValue !== "auto") {
|
|
8161
8225
|
this.style.setProperty("--aspect-ratio", newValue);
|
|
8162
8226
|
} else if (!newValue) {
|
|
8163
8227
|
this.style.removeProperty("--aspect-ratio");
|
|
8228
|
+
} else if (newValue === "auto" && this.#src) {
|
|
8229
|
+
this.#sniffDimensions(this.#src);
|
|
8164
8230
|
}
|
|
8165
8231
|
}
|
|
8232
|
+
|
|
8166
8233
|
if (name === "fit") {
|
|
8167
8234
|
if (newValue) {
|
|
8168
8235
|
this.style.setProperty("--fit", newValue);
|
|
@@ -8170,12 +8237,13 @@ class FigImage extends HTMLElement {
|
|
|
8170
8237
|
this.style.removeProperty("--fit");
|
|
8171
8238
|
}
|
|
8172
8239
|
}
|
|
8240
|
+
|
|
8173
8241
|
if (name === "checkerboard") {
|
|
8174
|
-
if (this
|
|
8242
|
+
if (this.#chit) {
|
|
8175
8243
|
if (newValue !== null && newValue !== "false") {
|
|
8176
|
-
this
|
|
8244
|
+
this.#chit.setAttribute("checkerboard", "");
|
|
8177
8245
|
} else {
|
|
8178
|
-
this
|
|
8246
|
+
this.#chit.removeAttribute("checkerboard");
|
|
8179
8247
|
}
|
|
8180
8248
|
}
|
|
8181
8249
|
}
|
|
@@ -8183,6 +8251,238 @@ class FigImage extends HTMLElement {
|
|
|
8183
8251
|
}
|
|
8184
8252
|
customElements.define("fig-image", FigImage);
|
|
8185
8253
|
|
|
8254
|
+
/* File Upload Input */
|
|
8255
|
+
class FigInputFile extends HTMLElement {
|
|
8256
|
+
static observedAttributes = ["accepts", "label", "disabled", "multiple", "variant", "url"];
|
|
8257
|
+
|
|
8258
|
+
#fileInput = null;
|
|
8259
|
+
#filenameEl = null;
|
|
8260
|
+
#clearBtn = null;
|
|
8261
|
+
#tooltipEl = null;
|
|
8262
|
+
#uploadBtn = null;
|
|
8263
|
+
#files = null;
|
|
8264
|
+
|
|
8265
|
+
get files() {
|
|
8266
|
+
return this.#files;
|
|
8267
|
+
}
|
|
8268
|
+
|
|
8269
|
+
get #urlFilename() {
|
|
8270
|
+
const url = this.getAttribute("url");
|
|
8271
|
+
if (!url) return "";
|
|
8272
|
+
try {
|
|
8273
|
+
const path = new URL(url, location.href).pathname;
|
|
8274
|
+
const name = path.split("/").pop();
|
|
8275
|
+
return name ? decodeURIComponent(name) : url;
|
|
8276
|
+
} catch {
|
|
8277
|
+
return url;
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
|
|
8281
|
+
get value() {
|
|
8282
|
+
if (this.#files && this.#files.length > 0) {
|
|
8283
|
+
if (this.#files.length === 1) return this.#files[0].name;
|
|
8284
|
+
return `${this.#files.length} files`;
|
|
8285
|
+
}
|
|
8286
|
+
return this.#urlFilename;
|
|
8287
|
+
}
|
|
8288
|
+
|
|
8289
|
+
connectedCallback() {
|
|
8290
|
+
this.#render();
|
|
8291
|
+
this.addEventListener("dragover", this.#onDragOver);
|
|
8292
|
+
this.addEventListener("dragleave", this.#onDragLeave);
|
|
8293
|
+
this.addEventListener("drop", this.#onDrop);
|
|
8294
|
+
}
|
|
8295
|
+
|
|
8296
|
+
disconnectedCallback() {
|
|
8297
|
+
this.#fileInput?.removeEventListener("change", this.#onFileChange);
|
|
8298
|
+
this.#clearBtn?.removeEventListener("click", this.#onClear);
|
|
8299
|
+
this.removeEventListener("dragover", this.#onDragOver);
|
|
8300
|
+
this.removeEventListener("dragleave", this.#onDragLeave);
|
|
8301
|
+
this.removeEventListener("drop", this.#onDrop);
|
|
8302
|
+
}
|
|
8303
|
+
|
|
8304
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
8305
|
+
if (oldValue === newValue) return;
|
|
8306
|
+
this.#render();
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
clear() {
|
|
8310
|
+
this.#files = null;
|
|
8311
|
+
if (this.#fileInput) this.#fileInput.value = "";
|
|
8312
|
+
this.removeAttribute("url");
|
|
8313
|
+
this.#render();
|
|
8314
|
+
this.#emitEvents();
|
|
8315
|
+
}
|
|
8316
|
+
|
|
8317
|
+
#emitEvents() {
|
|
8318
|
+
const detail = { files: this.#files };
|
|
8319
|
+
this.dispatchEvent(new CustomEvent("input", { detail, bubbles: true }));
|
|
8320
|
+
this.dispatchEvent(new CustomEvent("change", { detail, bubbles: true }));
|
|
8321
|
+
}
|
|
8322
|
+
|
|
8323
|
+
#onFileChange = () => {
|
|
8324
|
+
if (this.#fileInput.files.length > 0) {
|
|
8325
|
+
this.#files = this.#fileInput.files;
|
|
8326
|
+
this.removeAttribute("url");
|
|
8327
|
+
this.#render();
|
|
8328
|
+
this.#emitEvents();
|
|
8329
|
+
}
|
|
8330
|
+
};
|
|
8331
|
+
|
|
8332
|
+
#onClear = (e) => {
|
|
8333
|
+
e.stopPropagation();
|
|
8334
|
+
this.clear();
|
|
8335
|
+
};
|
|
8336
|
+
|
|
8337
|
+
#onDragOver = (e) => {
|
|
8338
|
+
e.preventDefault();
|
|
8339
|
+
this.setAttribute("dragover", "");
|
|
8340
|
+
};
|
|
8341
|
+
|
|
8342
|
+
#onDragLeave = () => {
|
|
8343
|
+
this.removeAttribute("dragover");
|
|
8344
|
+
};
|
|
8345
|
+
|
|
8346
|
+
#onDrop = (e) => {
|
|
8347
|
+
e.preventDefault();
|
|
8348
|
+
this.removeAttribute("dragover");
|
|
8349
|
+
if (
|
|
8350
|
+
this.hasAttribute("disabled") &&
|
|
8351
|
+
this.getAttribute("disabled") !== "false"
|
|
8352
|
+
)
|
|
8353
|
+
return;
|
|
8354
|
+
|
|
8355
|
+
const accepts = this.getAttribute("accepts");
|
|
8356
|
+
let dropped = Array.from(e.dataTransfer.files);
|
|
8357
|
+
if (accepts) {
|
|
8358
|
+
const allowed = accepts.split(",").map((s) => s.trim().toLowerCase());
|
|
8359
|
+
dropped = dropped.filter((file) => {
|
|
8360
|
+
const ext = "." + file.name.split(".").pop().toLowerCase();
|
|
8361
|
+
const mime = file.type.toLowerCase();
|
|
8362
|
+
return allowed.some(
|
|
8363
|
+
(a) =>
|
|
8364
|
+
a === ext ||
|
|
8365
|
+
a === mime ||
|
|
8366
|
+
(a.endsWith("/*") && mime.startsWith(a.slice(0, -1))),
|
|
8367
|
+
);
|
|
8368
|
+
});
|
|
8369
|
+
}
|
|
8370
|
+
if (!this.hasAttribute("multiple")) {
|
|
8371
|
+
dropped = dropped.slice(0, 1);
|
|
8372
|
+
}
|
|
8373
|
+
if (dropped.length === 0) return;
|
|
8374
|
+
|
|
8375
|
+
const dt = new DataTransfer();
|
|
8376
|
+
dropped.forEach((f) => dt.items.add(f));
|
|
8377
|
+
this.#files = dt.files;
|
|
8378
|
+
if (this.#fileInput) {
|
|
8379
|
+
this.#fileInput.files = dt.files;
|
|
8380
|
+
}
|
|
8381
|
+
this.removeAttribute("url");
|
|
8382
|
+
this.#render();
|
|
8383
|
+
this.#emitEvents();
|
|
8384
|
+
};
|
|
8385
|
+
|
|
8386
|
+
#render() {
|
|
8387
|
+
const accepts = this.getAttribute("accepts") || "";
|
|
8388
|
+
const label = this.getAttribute("label") || "Upload";
|
|
8389
|
+
const disabled =
|
|
8390
|
+
this.hasAttribute("disabled") &&
|
|
8391
|
+
this.getAttribute("disabled") !== "false";
|
|
8392
|
+
const multiple = this.hasAttribute("multiple");
|
|
8393
|
+
const variant = this.getAttribute("variant") || "input";
|
|
8394
|
+
const hasFile = (this.#files && this.#files.length > 0) || !!this.getAttribute("url");
|
|
8395
|
+
|
|
8396
|
+
this.innerHTML = "";
|
|
8397
|
+
|
|
8398
|
+
if (hasFile) {
|
|
8399
|
+
const tooltipText = accepts
|
|
8400
|
+
? `Accepts ${accepts
|
|
8401
|
+
.split(",")
|
|
8402
|
+
.map((s) => s.trim())
|
|
8403
|
+
.join(", ")}`
|
|
8404
|
+
: "";
|
|
8405
|
+
|
|
8406
|
+
this.#uploadBtn = document.createElement("fig-button");
|
|
8407
|
+
this.#uploadBtn.setAttribute("variant", variant);
|
|
8408
|
+
this.#uploadBtn.setAttribute("type", "upload");
|
|
8409
|
+
this.#uploadBtn.className = "fig-input-file-filename";
|
|
8410
|
+
if (disabled) this.#uploadBtn.setAttribute("disabled", "");
|
|
8411
|
+
const truncEl = document.createElement("fig-truncate");
|
|
8412
|
+
truncEl.setAttribute("position", "middle");
|
|
8413
|
+
truncEl.setAttribute("tooltip", "");
|
|
8414
|
+
const filename = this.value;
|
|
8415
|
+
const dotIdx = filename.lastIndexOf(".");
|
|
8416
|
+
if (dotIdx > 0) truncEl.setAttribute("tail", filename.slice(dotIdx));
|
|
8417
|
+
truncEl.textContent = filename;
|
|
8418
|
+
this.#uploadBtn.appendChild(truncEl);
|
|
8419
|
+
|
|
8420
|
+
this.#fileInput = document.createElement("input");
|
|
8421
|
+
this.#fileInput.type = "file";
|
|
8422
|
+
this.#fileInput.title = "";
|
|
8423
|
+
if (accepts) this.#fileInput.setAttribute("accept", accepts);
|
|
8424
|
+
if (multiple) this.#fileInput.setAttribute("multiple", "");
|
|
8425
|
+
this.#fileInput.addEventListener("change", this.#onFileChange);
|
|
8426
|
+
this.#uploadBtn.appendChild(this.#fileInput);
|
|
8427
|
+
|
|
8428
|
+
if (tooltipText) {
|
|
8429
|
+
this.#tooltipEl = document.createElement("fig-tooltip");
|
|
8430
|
+
this.#tooltipEl.setAttribute("text", tooltipText);
|
|
8431
|
+
this.#tooltipEl.appendChild(this.#uploadBtn);
|
|
8432
|
+
this.appendChild(this.#tooltipEl);
|
|
8433
|
+
} else {
|
|
8434
|
+
this.appendChild(this.#uploadBtn);
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
const clearTooltip = document.createElement("fig-tooltip");
|
|
8438
|
+
clearTooltip.setAttribute("text", "Remove");
|
|
8439
|
+
this.#clearBtn = document.createElement("fig-button");
|
|
8440
|
+
this.#clearBtn.setAttribute("variant", "ghost");
|
|
8441
|
+
this.#clearBtn.setAttribute("icon", "true");
|
|
8442
|
+
this.#clearBtn.className = "fig-input-file-clear";
|
|
8443
|
+
if (disabled) this.#clearBtn.setAttribute("disabled", "");
|
|
8444
|
+
this.#clearBtn.innerHTML = `<span class="fig-mask-icon" style="--icon: var(--icon-minus);"></span>`;
|
|
8445
|
+
this.#clearBtn.addEventListener("click", this.#onClear);
|
|
8446
|
+
clearTooltip.appendChild(this.#clearBtn);
|
|
8447
|
+
this.appendChild(clearTooltip);
|
|
8448
|
+
} else {
|
|
8449
|
+
const tooltipText = accepts
|
|
8450
|
+
? `Accepts ${accepts
|
|
8451
|
+
.split(",")
|
|
8452
|
+
.map((s) => s.trim())
|
|
8453
|
+
.join(", ")}`
|
|
8454
|
+
: "";
|
|
8455
|
+
|
|
8456
|
+
if (tooltipText) {
|
|
8457
|
+
this.#tooltipEl = document.createElement("fig-tooltip");
|
|
8458
|
+
this.#tooltipEl.setAttribute("text", tooltipText);
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
this.#uploadBtn = document.createElement("fig-button");
|
|
8462
|
+
this.#uploadBtn.setAttribute("variant", variant);
|
|
8463
|
+
this.#uploadBtn.setAttribute("type", "upload");
|
|
8464
|
+
this.#uploadBtn.textContent = label;
|
|
8465
|
+
if (disabled) this.#uploadBtn.setAttribute("disabled", "");
|
|
8466
|
+
|
|
8467
|
+
this.#fileInput = document.createElement("input");
|
|
8468
|
+
this.#fileInput.type = "file";
|
|
8469
|
+
this.#fileInput.title = "";
|
|
8470
|
+
if (accepts) this.#fileInput.setAttribute("accept", accepts);
|
|
8471
|
+
if (multiple) this.#fileInput.setAttribute("multiple", "");
|
|
8472
|
+
this.#fileInput.addEventListener("change", this.#onFileChange);
|
|
8473
|
+
this.#uploadBtn.appendChild(this.#fileInput);
|
|
8474
|
+
|
|
8475
|
+
if (this.#tooltipEl) {
|
|
8476
|
+
this.#tooltipEl.appendChild(this.#uploadBtn);
|
|
8477
|
+
this.appendChild(this.#tooltipEl);
|
|
8478
|
+
} else {
|
|
8479
|
+
this.appendChild(this.#uploadBtn);
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
}
|
|
8484
|
+
customElements.define("fig-input-file", FigInputFile);
|
|
8485
|
+
|
|
8186
8486
|
/**
|
|
8187
8487
|
* A bezier / spring easing curve editor with draggable control points.
|
|
8188
8488
|
* @attr {string} value - Bezier: "0.42, 0, 0.58, 1" or Spring: "spring(200, 15, 1)"
|
package/package.json
CHANGED