-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
import type * as Figma from "figma-js";
import { LitElement, html } from "lit";
import { property } from "lit/decorators.js";
import * as ErrorMessage from "./ErrorMessage";
import { ViewerMixin } from "./ViewerMixin";
import { SizedNode } from "./utils";
// TODO: Move docs for props in mixins (waiting for support at web-component-analyzer)
/**
* A Figma spec viewer. Displays a rendered image alongside sizing guides.
* @element figspec-frame-viewer
*
* @property {number} [panX=0]
* Current pan offset in px for X axis.
* This is a "before the scale" value.
*
* @property {number} [panY=0]
* Current pan offset in px for Y axis.
* This is a "before the scale" value.
*
* @property {number} [scale=1]
* Current zoom level, where 1.0 = 100%.
*
* @property {number} [zoomSpeed=500]
* How fast zooming when do ctrl+scroll / pinch gestures.
* Available values: 1 ~ 1000
* @attr [zoom-speed=500] See docs for `zoomSpeed` property.
*
* @property {number} [panSpeed=500]
* How fast panning when scroll vertically or horizontally.
* This does not affect to dragging with middle button pressed.
* Available values: 1 ~ 1000.
* @attr [pan-speed=500] See docs for `panSpeed` property.
*
* @property {Figma.Node | null} [selectedNode=null]
* Current selected node.
*
* @property {string} [link=null]
* Figma link for the given project/node. If passed, figspec will present a footer with metadata and a link to figma.
*
* @property {number} [zoomMargin=50]
* The minimum margin for the preview canvas in px. Will be used when the preview
* setting a default zooming scale for the canvas.
* @attr [zoom-margin=50] See docs for `zoomMargin` property.
*
* @fires scalechange When a user zoom-in or zoom-out the preview.
* @fires positionchange When a user panned the preview.
* @fires nodeselect When a user selected / unselected a node.
*/
export class FigspecFrameViewer extends ViewerMixin(LitElement) {
/**
* A response of "GET file nodes" API.
* https://www.figma.com/developers/api#get-file-nodes-endpoint
*/
@property({
type: Object,
})
nodes: Figma.FileNodesResponse | null = null;
/**
* An image rendered by "GET image" API.
* https://www.figma.com/developers/api#get-images-endpoint
*/
@property({
type: String,
attribute: "rendered-image",
})
renderedImage: string | null = null;
/** @private */
get isMovable(): boolean {
return !!(this.nodes && this.renderedImage && this.documentNode);
}
/**
* Readonly. Document node (= root drawable node).
* @readonly
*/
get documentNode(): SizedNode | null {
if (!this.nodes) {
return null;
}
const documentNode = Object.values(this.nodes.nodes)[0];
if (!documentNode || !("absoluteBoundingBox" in documentNode.document)) {
return null;
}
return documentNode.document;
}
/** @private */
get __images() {
if (!this.documentNode || !this.renderedImage) {
return {};
}
return {
[this.documentNode.id]: this.renderedImage,
};
}
/** @private */
get error() {
if (!this.nodes || !this.renderedImage) {
return ErrorMessage.ErrorMessage({
title: "Parameter error",
children: html`<span>
Both <code>nodes</code> and <code>rendered-image</code> are required.
</span>`,
});
}
if (!this.documentNode) {
return ErrorMessage.ErrorMessage({
title: "Parameter Error",
children: html`
<span> Document node is empty or does not have size. </span>
`,
});
}
if (super.error) {
return super.error;
}
}
getMetadata() {
return {
fileName: this.nodes!.name,
timestamp: this.nodes!.lastModified,
link: this.link,
};
}
connectedCallback() {
super.connectedCallback();
if (this.documentNode) {
this.__updateTree(this.documentNode);
this.__updateEffectMargins();
this.resetZoom();
}
}
updated(changedProperties: Parameters<LitElement["updated"]>[0]) {
super.updated(changedProperties);
if (changedProperties.has("nodes")) {
if (!this.documentNode) return;
this.__updateTree(this.documentNode);
this.resetZoom();
}
if (changedProperties.has("renderedImage")) {
this.__updateEffectMargins();
}
}
}