exsdk-ui5 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "exsdk-ui5",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "type": "module",
4
5
  "description": "Modern UI component SDK for SAP Fiori & UI5 — beautiful, fast, framework-independent",
5
6
  "author": "ExSDK",
6
7
  "license": "SEE LICENSE IN LICENSE.md",
@@ -17,6 +18,7 @@
17
18
  },
18
19
  "files": [
19
20
  "dist",
21
+ "src/controls",
20
22
  "ui5-wrapper",
21
23
  "README.md",
22
24
  "LICENSE.md"
@@ -0,0 +1,210 @@
1
+ sap.ui.define([
2
+ "sap/ui/core/Control"
3
+ ], function (Control) {
4
+ "use strict";
5
+
6
+ return Control.extend("project1.control.ExButton", {
7
+
8
+ metadata: {
9
+ properties: {
10
+ text: { type: "string", defaultValue: "Button" },
11
+ // primary | secondary | ghost | danger | pill | gradient | glass | text-btn
12
+ type: { type: "string", defaultValue: "primary" },
13
+ // sm | md | lg
14
+ size: { type: "string", defaultValue: "md" },
15
+ icon: { type: "string", defaultValue: "" },
16
+ iconPos: { type: "string", defaultValue: "left" }, // left | right
17
+ iconOnly: { type: "boolean", defaultValue: false },
18
+ loading: { type: "boolean", defaultValue: false },
19
+ enabled: { type: "boolean", defaultValue: true },
20
+ // success | error | idle
21
+ state: { type: "string", defaultValue: "idle" },
22
+ width: { type: "string", defaultValue: "" } // "full" veya px değeri
23
+ },
24
+ events: {
25
+ press: {}
26
+ }
27
+ },
28
+
29
+ renderer: function (oRM, oControl) {
30
+ oRM.openStart("div", oControl);
31
+ oRM.class("ex-btn-wrapper");
32
+ oRM.openEnd();
33
+ oRM.close("div");
34
+ },
35
+
36
+ onAfterRendering: function () {
37
+ this._render();
38
+ },
39
+
40
+ // =============================
41
+ // RENDER
42
+ // =============================
43
+ _render: function () {
44
+ const oWrapper = this.getDomRef();
45
+ if (!oWrapper) return;
46
+
47
+ const type = this.getType();
48
+ const size = this.getSize();
49
+ const text = this.getText();
50
+ const icon = this.getIcon();
51
+ const iconPos = this.getIconPos();
52
+ const iconOnly = this.getIconOnly();
53
+ const loading = this.getLoading();
54
+ const enabled = this.getEnabled();
55
+ const state = this.getState();
56
+ const width = this.getWidth();
57
+
58
+ const isDisabled = !enabled || loading;
59
+
60
+ // State'e göre icon & metin
61
+ let displayText = text;
62
+ let displayIcon = icon;
63
+
64
+ if (state === "success") {
65
+ displayText = "Başarılı";
66
+ displayIcon = "✓";
67
+ } else if (state === "error") {
68
+ displayText = "Hata";
69
+ displayIcon = "✕";
70
+ } else if (loading) {
71
+ displayText = "Yükleniyor...";
72
+ }
73
+
74
+ const spinnerHTML = `
75
+ <span class="ex-btn-icon ex-btn-spinner-icon">
76
+ <svg viewBox="0 0 20 20" fill="none">
77
+ <circle cx="10" cy="10" r="7"
78
+ stroke="currentColor"
79
+ stroke-width="2.5"
80
+ stroke-linecap="round"
81
+ stroke-dasharray="38"
82
+ stroke-dashoffset="28"/>
83
+ </svg>
84
+ </span>`;
85
+
86
+ const iconHTML = displayIcon
87
+ ? `<span class="ex-btn-icon">${displayIcon}</span>`
88
+ : "";
89
+
90
+ const textHTML = (!iconOnly || loading || state !== "idle")
91
+ ? `<span class="ex-btn-text">${displayText}</span>`
92
+ : "";
93
+
94
+ const leftContent = (iconPos === "left" && !loading) ? iconHTML : "";
95
+ const rightContent = (iconPos === "right" && !loading) ? iconHTML : "";
96
+ const loaderHTML = loading ? spinnerHTML : "";
97
+
98
+ const widthStyle = width === "full"
99
+ ? "width:100%;"
100
+ : width ? `width:${width};` : "";
101
+
102
+ const stateClass = state !== "idle" ? `ex-btn--${state}` : "";
103
+
104
+ oWrapper.innerHTML = `
105
+ <button
106
+ class="ex-btn ex-btn--${type} ex-btn--${size} ${stateClass} ${iconOnly && state === "idle" && !loading ? "ex-btn--icon-only" : ""} ${loading ? "ex-btn--loading" : ""}"
107
+ style="${widthStyle}"
108
+ ${isDisabled ? "disabled" : ""}
109
+ aria-busy="${loading}"
110
+ aria-label="${text}">
111
+ <span class="ex-btn-ripple-container"></span>
112
+ ${loaderHTML}
113
+ ${leftContent}
114
+ ${textHTML}
115
+ ${rightContent}
116
+ </button>
117
+ `;
118
+
119
+ this._attachEvents(oWrapper);
120
+ },
121
+
122
+ // =============================
123
+ // EVENTS
124
+ // =============================
125
+ _attachEvents: function (oWrapper) {
126
+ const btn = oWrapper.querySelector("button");
127
+ if (!btn) return;
128
+
129
+ btn.addEventListener("click", (e) => {
130
+ if (btn.disabled) return;
131
+ this._ripple(btn, e);
132
+ this.firePress();
133
+ });
134
+
135
+ btn.addEventListener("keydown", (e) => {
136
+ if ((e.key === "Enter" || e.key === " ") && !btn.disabled) {
137
+ e.preventDefault();
138
+ this._ripple(btn, null);
139
+ this.firePress();
140
+ }
141
+ });
142
+ },
143
+
144
+ // =============================
145
+ // RIPPLE
146
+ // =============================
147
+ _ripple: function (btn, e) {
148
+ const container = btn.querySelector(".ex-btn-ripple-container");
149
+ if (!container) return;
150
+
151
+ const circle = document.createElement("span");
152
+ circle.className = "ex-btn-ripple";
153
+
154
+ const rect = btn.getBoundingClientRect();
155
+ const size = Math.max(rect.width, rect.height) * 2;
156
+ const x = e ? e.clientX - rect.left - size / 2 : rect.width / 2 - size / 2;
157
+ const y = e ? e.clientY - rect.top - size / 2 : rect.height / 2 - size / 2;
158
+
159
+ circle.style.cssText = `width:${size}px; height:${size}px; left:${x}px; top:${y}px;`;
160
+ container.appendChild(circle);
161
+ circle.addEventListener("animationend", () => circle.remove());
162
+ },
163
+
164
+ // =============================
165
+ // PUBLIC SETTERS
166
+ // =============================
167
+ setText: function (s) {
168
+ this.setProperty("text", s, true);
169
+ const el = this._getEl(".ex-btn-text");
170
+ if (el) el.textContent = s;
171
+ },
172
+
173
+ setLoading: function (b) {
174
+ this.setProperty("loading", b, true);
175
+ this._render();
176
+ },
177
+
178
+ setEnabled: function (b) {
179
+ this.setProperty("enabled", b, true);
180
+ const btn = this._getEl("button");
181
+ if (btn) btn.disabled = !b || this.getLoading();
182
+ },
183
+
184
+ setType: function (s) {
185
+ this.setProperty("type", s, true);
186
+ this._render();
187
+ },
188
+
189
+ setState: function (s) {
190
+ this.setProperty("state", s, true);
191
+ this._render();
192
+
193
+ // success / error sonrası otomatik idle'a dön
194
+ if (s === "success" || s === "error") {
195
+ setTimeout(() => {
196
+ this.setProperty("state", "idle", true);
197
+ this._render();
198
+ }, 2000);
199
+ }
200
+ },
201
+
202
+ // =============================
203
+ // UTILS
204
+ // =============================
205
+ _getEl: function (sel) {
206
+ const w = this.getDomRef();
207
+ return w ? w.querySelector(sel) : null;
208
+ }
209
+ });
210
+ });
@@ -0,0 +1,209 @@
1
+ sap.ui.define([
2
+ "sap/ui/core/Control"
3
+ ], function (Control) {
4
+ "use strict";
5
+
6
+ return Control.extend("project1.control.ExTable", {
7
+
8
+ metadata: {
9
+ properties: {
10
+ columns: { type: "object", defaultValue: [] },
11
+ rows: { type: "object", defaultValue: [] },
12
+ title: { type: "string", defaultValue: "Tablo" },
13
+ total: { type: "int", defaultValue: 0 }
14
+ },
15
+ events: {
16
+ loadMore: {}
17
+ }
18
+ },
19
+
20
+ renderer: function (oRM, oControl) {
21
+ oRM.openStart("div", oControl);
22
+ oRM.class("ex-table-wrapper");
23
+ oRM.openEnd();
24
+ oRM.close("div");
25
+ },
26
+
27
+ onAfterRendering: function () {
28
+ const oWrapper = this.getDomRef();
29
+ oWrapper.innerHTML = this._buildBaseHTML();
30
+
31
+ this._body = oWrapper.querySelector(".ex-table-body");
32
+ this._tbody = oWrapper.querySelector(".ex-table-tbody");
33
+ this._thead = oWrapper.querySelector(".ex-table-thead");
34
+ this._colgroup = oWrapper.querySelector(".ex-table-colgroup");
35
+ this._totalEl = oWrapper.querySelector(".ex-table-total");
36
+
37
+ this._rowHeight = 44;
38
+ this._visibleCount = 20;
39
+ this._buffer = 5;
40
+
41
+ this._fullData = this.getRows() || [];
42
+ this._columns = this.getColumns();
43
+ this._sortState = {};
44
+
45
+ this._attachEvents();
46
+ this._renderColgroup();
47
+ this._renderHeader();
48
+ this._updateVisibleRows(0);
49
+ },
50
+
51
+ _buildBaseHTML: function () {
52
+ return `
53
+ <div class="ex-table">
54
+ <div class="ex-table-header">
55
+ <span class="ex-table-title">${this.getTitle()}</span>
56
+ <span class="ex-table-total">(${this.getTotal()} Ürün)</span>
57
+ </div>
58
+ <div class="ex-table-body">
59
+ <table class="ex-table-inner">
60
+ <colgroup class="ex-table-colgroup"></colgroup>
61
+ <thead class="ex-table-thead"></thead>
62
+ <tbody class="ex-table-tbody"></tbody>
63
+ </table>
64
+ </div>
65
+ </div>
66
+ `;
67
+ },
68
+
69
+ // colgroup — her kolon için eşit genişlik tanımla
70
+ // th ve td artık aynı col'u paylaşır, hizalama garantili
71
+ _renderColgroup: function () {
72
+ const colWidth = Math.floor(100 / this._columns.length);
73
+ this._colgroup.innerHTML = this._columns.map(() =>
74
+ `<col style="width:${colWidth}%;">`
75
+ ).join("");
76
+ },
77
+
78
+ _attachEvents: function () {
79
+ this._body.addEventListener("scroll", this._onScroll.bind(this));
80
+ },
81
+
82
+ _onScroll: function () {
83
+ const scrollTop = this._body.scrollTop;
84
+ const startIndex = Math.floor(scrollTop / this._rowHeight);
85
+ this._updateVisibleRows(startIndex);
86
+
87
+ if (scrollTop + this._body.clientHeight >= this._body.scrollHeight - 100) {
88
+ this.fireLoadMore();
89
+ }
90
+ },
91
+
92
+ _renderHeader: function () {
93
+ this._thead.innerHTML = `<tr>
94
+ ${this._columns.map(col => `
95
+ <th data-key="${col.key}">
96
+ <div class="ex-th-inner">
97
+ <span class="ex-th-label">${col.label}</span>
98
+ <span class="ex-th-sort" data-key="${col.key}">↕</span>
99
+ <span class="resize-handle"></span>
100
+ </div>
101
+ </th>
102
+ `).join("")}
103
+ </tr>`;
104
+
105
+ this._attachHeaderEvents();
106
+ },
107
+
108
+ _attachHeaderEvents: function () {
109
+ this._thead.querySelectorAll("th").forEach(th => {
110
+ th.addEventListener("click", (e) => {
111
+ if (e.target.classList.contains("resize-handle")) return;
112
+ this._sort(th.dataset.key);
113
+ });
114
+
115
+ const handle = th.querySelector(".resize-handle");
116
+ let startX, startWidth, colIndex;
117
+
118
+ handle.addEventListener("mousedown", (e) => {
119
+ e.stopPropagation();
120
+ startX = e.pageX;
121
+ startWidth = th.offsetWidth;
122
+ colIndex = Array.from(th.parentNode.children).indexOf(th);
123
+
124
+ const onMove = (e) => {
125
+ const newWidth = Math.max(60, startWidth + (e.pageX - startX));
126
+ // colgroup'taki ilgili col'u güncelle — hem th hem td hizalanır
127
+ const cols = this._colgroup.querySelectorAll("col");
128
+ if (cols[colIndex]) {
129
+ cols[colIndex].style.width = newWidth + "px";
130
+ cols[colIndex].style.minWidth = newWidth + "px";
131
+ }
132
+ };
133
+ const onUp = () => {
134
+ document.removeEventListener("mousemove", onMove);
135
+ document.removeEventListener("mouseup", onUp);
136
+ };
137
+
138
+ document.addEventListener("mousemove", onMove);
139
+ document.addEventListener("mouseup", onUp);
140
+ });
141
+ });
142
+ },
143
+
144
+ _sort: function (key) {
145
+ const next = (this._sortState[key] || "asc") === "asc" ? "desc" : "asc";
146
+ this._sortState = { [key]: next };
147
+
148
+ this._thead.querySelectorAll(".ex-th-sort").forEach(el => {
149
+ el.textContent = el.dataset.key === key
150
+ ? (next === "asc" ? "↑" : "↓")
151
+ : "↕";
152
+ });
153
+
154
+ this._fullData.sort((a, b) => {
155
+ if (a[key] < b[key]) return next === "asc" ? -1 : 1;
156
+ if (a[key] > b[key]) return next === "asc" ? 1 : -1;
157
+ return 0;
158
+ });
159
+
160
+ this._body.scrollTop = 0;
161
+ this._updateVisibleRows(0);
162
+ },
163
+
164
+ _updateVisibleRows: function (startIndex) {
165
+ const total = this._fullData.length;
166
+ const start = Math.max(0, startIndex - this._buffer);
167
+ const end = Math.min(total, startIndex + this._visibleCount + this._buffer);
168
+
169
+ const paddingTop = start * this._rowHeight;
170
+ const paddingBottom = Math.max(0, (total - end) * this._rowHeight);
171
+
172
+ this._renderRows(this._fullData.slice(start, end), paddingTop, paddingBottom);
173
+ },
174
+
175
+ _renderRows: function (rows, paddingTop, paddingBottom) {
176
+ const colSpan = this._columns.length;
177
+
178
+ const topRow = paddingTop > 0 ? `<tr style="height:${paddingTop}px;"> <td colspan="${colSpan}"></td></tr>` : "";
179
+ const bottomRow = paddingBottom > 0 ? `<tr style="height:${paddingBottom}px;"><td colspan="${colSpan}"></td></tr>` : "";
180
+
181
+ const dataRows = rows.length > 0
182
+ ? rows.map(row => `
183
+ <tr class="ex-table-row">
184
+ ${this._columns.map(col =>
185
+ `<td class="ex-td">${row[col.key] ?? ""}</td>`
186
+ ).join("")}
187
+ </tr>`).join("")
188
+ : `<tr><td colspan="${colSpan}" class="ex-td-empty">Veri yok</td></tr>`;
189
+
190
+ this._tbody.innerHTML = topRow + dataRows + bottomRow;
191
+ },
192
+
193
+ setRows: function (aRows) {
194
+ this.setProperty("rows", aRows, true);
195
+ this._fullData = aRows;
196
+ if (this._body) {
197
+ this._body.scrollTop = 0;
198
+ this._updateVisibleRows(0);
199
+ }
200
+ },
201
+
202
+ setTotal: function (iTotal) {
203
+ this.setProperty("total", iTotal, true);
204
+ if (this._totalEl) {
205
+ this._totalEl.textContent = "(" + iTotal + " Ürün)";
206
+ }
207
+ }
208
+ });
209
+ });