@dodlhuat/basix 1.3.0 → 1.3.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/js/editor.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { sanitizeHtml } from './utils.js';
1
2
  class Editor {
2
3
  constructor(options = {}) {
3
4
  this.undoStack = [];
@@ -89,11 +90,11 @@ class Editor {
89
90
  const code = this.code;
90
91
  const codeActions = document.querySelectorAll('.code-actions button');
91
92
  codeActions[0]?.addEventListener('click', () => {
92
- this.editable.innerHTML = this.sanitizeHTML(code.value);
93
+ this.editable.innerHTML = sanitizeHtml(code.value);
93
94
  this.onContentChange();
94
95
  }, sig);
95
96
  codeActions[1]?.addEventListener('click', () => {
96
- code.value = this.sanitizeHTML(code.value);
97
+ code.value = sanitizeHtml(code.value);
97
98
  this.editable.innerHTML = code.value;
98
99
  this.onContentChange();
99
100
  }, sig);
@@ -183,7 +184,7 @@ class Editor {
183
184
  if (this.code)
184
185
  this.code.value = this.editable.innerHTML.trim();
185
186
  if (this.preview)
186
- this.preview.innerHTML = this.sanitizeHTML(this.editable.innerHTML);
187
+ this.preview.innerHTML = sanitizeHtml(this.editable.innerHTML);
187
188
  this.updateWordCount();
188
189
  }
189
190
  updateWordCount() {
@@ -407,21 +408,8 @@ class Editor {
407
408
  sel.addRange(range);
408
409
  this.onContentChange();
409
410
  }
410
- sanitizeHTML(html) {
411
- const parser = new DOMParser();
412
- const doc = parser.parseFromString(html, 'text/html');
413
- doc.querySelectorAll('script, style, iframe, object, embed').forEach(el => el.remove());
414
- doc.querySelectorAll('*').forEach(el => {
415
- for (const attr of Array.from(el.attributes)) {
416
- if (attr.name.startsWith('on') || attr.value.trim().toLowerCase().startsWith('javascript:')) {
417
- el.removeAttribute(attr.name);
418
- }
419
- }
420
- });
421
- return doc.body.innerHTML;
422
- }
423
411
  downloadHTML() {
424
- const content = this.sanitizeHTML(this.editable.innerHTML);
412
+ const content = sanitizeHtml(this.editable.innerHTML);
425
413
  const html = `<!doctype html>
426
414
  <html lang="en">
427
415
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Export</title></head>
package/js/gallery.d.ts CHANGED
@@ -22,6 +22,7 @@ declare class MasonryGallery {
22
22
  constructor(containerId: string, options: MasonryGalleryOptions);
23
23
  private init;
24
24
  private setupLayout;
25
+ private buildColumns;
25
26
  private addEventListeners;
26
27
  private reLayout;
27
28
  private handleScroll;
package/js/gallery.js CHANGED
@@ -37,14 +37,17 @@ class MasonryGallery {
37
37
  const containerWidth = this.container.getBoundingClientRect().width;
38
38
  const numColumns = Math.max(1, Math.floor(containerWidth / this.options.minColumnWidth));
39
39
  if (this.columns.length !== numColumns) {
40
- this.container.innerHTML = "";
41
- this.columns = [];
42
- for (let i = 0; i < numColumns; i++) {
43
- const col = document.createElement("div");
44
- col.className = "masonry-column";
45
- this.container.appendChild(col);
46
- this.columns.push(col);
47
- }
40
+ this.buildColumns(numColumns);
41
+ }
42
+ }
43
+ buildColumns(count) {
44
+ this.container.innerHTML = '';
45
+ this.columns = [];
46
+ for (let i = 0; i < count; i++) {
47
+ const col = document.createElement('div');
48
+ col.className = 'masonry-column';
49
+ this.container.appendChild(col);
50
+ this.columns.push(col);
48
51
  }
49
52
  }
50
53
  addEventListeners() {
@@ -62,28 +65,16 @@ class MasonryGallery {
62
65
  });
63
66
  }
64
67
  reLayout() {
65
- const items = [];
66
- this.columns.forEach((col) => {
67
- Array.from(col.children).forEach((child) => {
68
- items.push(child);
69
- });
70
- col.innerHTML = "";
71
- });
68
+ const items = this.columns.flatMap(col => Array.from(col.children));
72
69
  const availableWidth = Math.min(1200, window.innerWidth - 40);
73
70
  const numColumns = Math.max(1, Math.floor(availableWidth / this.options.minColumnWidth));
74
71
  if (this.columns.length !== numColumns) {
75
- this.container.innerHTML = "";
76
- this.columns = [];
77
- for (let i = 0; i < numColumns; i++) {
78
- const col = document.createElement("div");
79
- col.className = "masonry-column";
80
- this.container.appendChild(col);
81
- this.columns.push(col);
82
- }
72
+ this.buildColumns(numColumns);
83
73
  }
84
- items.forEach((item) => {
85
- this.addToShortestColumn(item);
86
- });
74
+ else {
75
+ this.columns.forEach(col => { col.innerHTML = ''; });
76
+ }
77
+ items.forEach(item => this.addToShortestColumn(item));
87
78
  }
88
79
  async loadMoreImages(isAutoFill = false) {
89
80
  if (!isAutoFill)
package/js/push-menu.d.ts CHANGED
@@ -19,7 +19,6 @@ declare class PushMenu {
19
19
  private static resetPanels;
20
20
  private static handleNavigationChange;
21
21
  static pushToggle(): void;
22
- private static toggleClass;
23
22
  private static clickNav;
24
23
  private static handleBackdropClick;
25
24
  static open(): void;
package/js/push-menu.js CHANGED
@@ -139,10 +139,10 @@ class PushMenu {
139
139
  throw new Error('PushMenu: Required elements not found (.push-content, .push-menu)');
140
140
  }
141
141
  const isPushed = this.elements.content.classList.contains('pushed');
142
- this.toggleClass(this.elements.content, 'pushed', !isPushed);
143
- this.toggleClass(this.elements.menu, 'pushed', !isPushed);
144
- this.toggleClass(this.elements.header, 'pushed', !isPushed);
145
- this.toggleClass(this.elements.backdrop, 'pushed', !isPushed);
142
+ this.elements.content.classList.toggle('pushed', !isPushed);
143
+ this.elements.menu.classList.toggle('pushed', !isPushed);
144
+ this.elements.header?.classList.toggle('pushed', !isPushed);
145
+ this.elements.backdrop?.classList.toggle('pushed', !isPushed);
146
146
  if (this.elements.controlIcon) {
147
147
  if (isPushed) {
148
148
  this.elements.controlIcon.classList.remove('icon-menu_open');
@@ -154,16 +154,6 @@ class PushMenu {
154
154
  }
155
155
  }
156
156
  }
157
- static toggleClass(element, className, add) {
158
- if (!element)
159
- return;
160
- if (add) {
161
- element.classList.add(className);
162
- }
163
- else {
164
- element.classList.remove(className);
165
- }
166
- }
167
157
  static open() {
168
158
  if (!this.elements.content?.classList.contains('pushed')) {
169
159
  this.pushToggle();
@@ -16,17 +16,31 @@ declare class TimeSpanPicker {
16
16
  private onChange?;
17
17
  private readonly uid;
18
18
  private fromString;
19
- private toString;
19
+ private toLabel;
20
+ private pickerEl;
21
+ private durationEl;
22
+ private barEl;
23
+ private barFillEl;
24
+ private startHandleEl;
25
+ private endHandleEl;
26
+ private dragState;
20
27
  constructor(elementOrSelector: string | HTMLElement, options?: TimeSpanPickerOptions);
21
- private queryInput;
28
+ private queryEl;
22
29
  private render;
23
- private readonly handleStartChange;
24
- private readonly handleEndChange;
30
+ private readonly handleChange;
25
31
  private attachEventListeners;
32
+ private attachBarListeners;
33
+ private beginDrag;
34
+ private readonly onStartHandleDown;
35
+ private readonly onEndHandleDown;
36
+ private readonly onFillDown;
37
+ private readonly onPointerMove;
38
+ private readonly onPointerUp;
26
39
  private toMinutes;
40
+ private minutesToTime;
41
+ private snap;
27
42
  private formatDuration;
28
43
  private updateUI;
29
- private handleChange;
30
44
  getValue(): TimeSpan;
31
45
  setValue(start: string, end: string): void;
32
46
  reset(): void;
package/js/timepicker.js CHANGED
@@ -1,7 +1,72 @@
1
1
  class TimeSpanPicker {
2
2
  constructor(elementOrSelector, options) {
3
- this.handleStartChange = () => { this.handleChange(); };
4
- this.handleEndChange = () => { this.handleChange(); };
3
+ this.dragState = null;
4
+ this.handleChange = () => {
5
+ this.updateUI();
6
+ const start = this.startTimeInput.value;
7
+ const end = this.endTimeInput.value;
8
+ if (this.onChange && start && end) {
9
+ this.onChange(start, end);
10
+ }
11
+ };
12
+ this.onStartHandleDown = (e) => {
13
+ e.stopPropagation();
14
+ e.preventDefault();
15
+ this.beginDrag('start');
16
+ };
17
+ this.onEndHandleDown = (e) => {
18
+ e.stopPropagation();
19
+ e.preventDefault();
20
+ this.beginDrag('end');
21
+ };
22
+ this.onFillDown = (e) => {
23
+ if (e.target.classList.contains('timespan-handle'))
24
+ return;
25
+ e.preventDefault();
26
+ const start = this.startTimeInput.value;
27
+ if (!start)
28
+ return;
29
+ const rect = this.barEl.getBoundingClientRect();
30
+ const clickMins = ((e.clientX - rect.left) / rect.width) * 1440;
31
+ this.beginDrag('move', clickMins - this.toMinutes(start), rect);
32
+ };
33
+ this.onPointerMove = (e) => {
34
+ if (!this.dragState)
35
+ return;
36
+ e.preventDefault();
37
+ const { type, barLeft, barWidth, startMins, endMins, clickOffsetMins } = this.dragState;
38
+ const pct = Math.max(0, Math.min(1, (e.clientX - barLeft) / barWidth));
39
+ const rawMins = pct * 1440;
40
+ let start = this.startTimeInput.value;
41
+ let end = this.endTimeInput.value;
42
+ if (type === 'start') {
43
+ start = this.minutesToTime(Math.max(0, Math.min(endMins - 5, this.snap(rawMins))));
44
+ this.startTimeInput.value = start;
45
+ }
46
+ else if (type === 'end') {
47
+ end = this.minutesToTime(Math.max(startMins + 5, Math.min(1440, this.snap(rawMins))));
48
+ this.endTimeInput.value = end;
49
+ }
50
+ else {
51
+ const duration = endMins - startMins;
52
+ const newStartMins = Math.max(0, Math.min(1440 - duration, this.snap(rawMins - clickOffsetMins)));
53
+ start = this.minutesToTime(newStartMins);
54
+ end = this.minutesToTime(newStartMins + duration);
55
+ this.startTimeInput.value = start;
56
+ this.endTimeInput.value = end;
57
+ }
58
+ this.updateUI();
59
+ if (this.onChange)
60
+ this.onChange(start, end);
61
+ };
62
+ this.onPointerUp = () => {
63
+ if (!this.dragState)
64
+ return;
65
+ this.dragState = null;
66
+ this.barEl.classList.remove('is-dragging');
67
+ document.removeEventListener('pointermove', this.onPointerMove);
68
+ document.removeEventListener('pointerup', this.onPointerUp);
69
+ };
5
70
  const element = typeof elementOrSelector === 'string'
6
71
  ? (elementOrSelector.startsWith('#') || elementOrSelector.startsWith('.')
7
72
  ? document.querySelector(elementOrSelector)
@@ -14,28 +79,25 @@ class TimeSpanPicker {
14
79
  this.onChange = options?.onChange;
15
80
  this.uid = `tsp-${Math.random().toString(36).slice(2, 9)}`;
16
81
  this.fromString = options?.fromString ?? 'From';
17
- this.toString = options?.toString ?? 'To';
82
+ this.toLabel = options?.toString ?? 'To';
18
83
  this.render();
19
- this.startTimeInput = this.queryInput('.timespan-start');
20
- this.endTimeInput = this.queryInput('.timespan-end');
21
- if (options?.defaultStart) {
84
+ this.startTimeInput = this.queryEl('.timespan-start');
85
+ this.endTimeInput = this.queryEl('.timespan-end');
86
+ if (options?.defaultStart)
22
87
  this.startTimeInput.value = options.defaultStart;
23
- }
24
- if (options?.defaultEnd) {
88
+ if (options?.defaultEnd)
25
89
  this.endTimeInput.value = options.defaultEnd;
26
- }
27
90
  this.attachEventListeners();
28
- // Render initial state if defaults provided
91
+ this.attachBarListeners();
29
92
  if (options?.defaultStart || options?.defaultEnd) {
30
93
  this.updateUI();
31
94
  }
32
95
  }
33
- queryInput(selector) {
34
- const input = this.container.querySelector(selector);
35
- if (!input) {
36
- throw new Error(`Input with selector "${selector}" not found`);
37
- }
38
- return input;
96
+ queryEl(selector) {
97
+ const el = this.container.querySelector(selector);
98
+ if (!el)
99
+ throw new Error(`TimeSpanPicker: "${selector}" not found`);
100
+ return el;
39
101
  }
40
102
  render() {
41
103
  const startId = `${this.uid}-start`;
@@ -53,23 +115,63 @@ class TimeSpanPicker {
53
115
  </div>
54
116
 
55
117
  <div class="timespan-field timespan-field-end">
56
- <label for="${endId}">${this.toString}</label>
118
+ <label for="${endId}">${this.toLabel}</label>
57
119
  <input type="time" class="timespan-end" id="${endId}"/>
58
120
  </div>
59
121
  </div>
60
122
  <div class="timespan-bar" aria-hidden="true">
61
- <div class="timespan-bar-fill"></div>
123
+ <div class="timespan-bar-fill">
124
+ <div class="timespan-handle timespan-handle-start"></div>
125
+ <div class="timespan-handle timespan-handle-end"></div>
126
+ </div>
62
127
  </div>
63
128
  `;
64
129
  }
65
130
  attachEventListeners() {
66
- this.startTimeInput.addEventListener('change', this.handleStartChange);
67
- this.endTimeInput.addEventListener('change', this.handleEndChange);
131
+ this.startTimeInput.addEventListener('change', this.handleChange);
132
+ this.endTimeInput.addEventListener('change', this.handleChange);
133
+ }
134
+ attachBarListeners() {
135
+ this.pickerEl = this.queryEl('.timespan-picker');
136
+ this.durationEl = this.queryEl('.timespan-duration');
137
+ this.barEl = this.queryEl('.timespan-bar');
138
+ this.barFillEl = this.queryEl('.timespan-bar-fill');
139
+ this.startHandleEl = this.queryEl('.timespan-handle-start');
140
+ this.endHandleEl = this.queryEl('.timespan-handle-end');
141
+ this.startHandleEl.addEventListener('pointerdown', this.onStartHandleDown);
142
+ this.endHandleEl.addEventListener('pointerdown', this.onEndHandleDown);
143
+ this.barFillEl.addEventListener('pointerdown', this.onFillDown);
144
+ }
145
+ beginDrag(type, clickOffsetMins = 0, rect) {
146
+ const start = this.startTimeInput.value;
147
+ const end = this.endTimeInput.value;
148
+ if (!start || !end)
149
+ return;
150
+ const barRect = rect ?? this.barEl.getBoundingClientRect();
151
+ this.dragState = {
152
+ type,
153
+ barLeft: barRect.left,
154
+ barWidth: barRect.width,
155
+ startMins: this.toMinutes(start),
156
+ endMins: this.toMinutes(end),
157
+ clickOffsetMins,
158
+ };
159
+ this.barEl.classList.add('is-dragging');
160
+ document.addEventListener('pointermove', this.onPointerMove);
161
+ document.addEventListener('pointerup', this.onPointerUp);
68
162
  }
69
163
  toMinutes(time) {
70
164
  const [h, m] = time.split(':').map(Number);
71
165
  return h * 60 + m;
72
166
  }
167
+ minutesToTime(minutes) {
168
+ const h = Math.floor(minutes / 60);
169
+ const m = minutes % 60;
170
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
171
+ }
172
+ snap(mins) {
173
+ return Math.round(mins / 5) * 5;
174
+ }
73
175
  formatDuration(minutes) {
74
176
  const h = Math.floor(minutes / 60);
75
177
  const m = minutes % 60;
@@ -80,44 +182,31 @@ class TimeSpanPicker {
80
182
  return `${m}m`;
81
183
  }
82
184
  updateUI() {
83
- const picker = this.container.querySelector('.timespan-picker');
84
- const durationEl = this.container.querySelector('.timespan-duration');
85
- const barFill = this.container.querySelector('.timespan-bar-fill');
86
185
  const start = this.startTimeInput.value;
87
186
  const end = this.endTimeInput.value;
88
187
  const isError = !!(start && end && start >= end);
89
- picker?.classList.toggle('is-error', isError);
188
+ this.pickerEl.classList.toggle('is-error', isError);
90
189
  if (isError) {
91
190
  this.endTimeInput.setCustomValidity('End time must be after start time');
92
- if (durationEl)
93
- durationEl.textContent = '!';
191
+ this.durationEl.textContent = '!';
192
+ this.barFillEl.classList.remove('is-active');
94
193
  return;
95
194
  }
96
195
  this.endTimeInput.setCustomValidity('');
97
- if (start && end && durationEl && barFill) {
196
+ if (start && end) {
98
197
  const startMins = this.toMinutes(start);
99
198
  const endMins = this.toMinutes(end);
100
199
  const duration = endMins - startMins;
101
- durationEl.textContent = this.formatDuration(duration);
102
- const startPct = ((startMins / 1440) * 100).toFixed(2);
103
- const widthPct = ((duration / 1440) * 100).toFixed(2);
104
- barFill.style.left = `${startPct}%`;
105
- barFill.style.width = `${widthPct}%`;
200
+ this.durationEl.textContent = this.formatDuration(duration);
201
+ this.barFillEl.style.left = `${((startMins / 1440) * 100).toFixed(2)}%`;
202
+ this.barFillEl.style.width = `${((duration / 1440) * 100).toFixed(2)}%`;
203
+ this.barFillEl.classList.add('is-active');
106
204
  }
107
205
  else {
108
- if (durationEl)
109
- durationEl.textContent = '';
110
- if (barFill) {
111
- barFill.style.left = '0';
112
- barFill.style.width = '0';
113
- }
114
- }
115
- }
116
- handleChange() {
117
- this.updateUI();
118
- const { start, end } = this.getValue();
119
- if (this.onChange && start && end) {
120
- this.onChange(start, end);
206
+ this.durationEl.textContent = '';
207
+ this.barFillEl.style.left = '0';
208
+ this.barFillEl.style.width = '0';
209
+ this.barFillEl.classList.remove('is-active');
121
210
  }
122
211
  }
123
212
  getValue() {
@@ -135,24 +224,24 @@ class TimeSpanPicker {
135
224
  this.startTimeInput.value = '';
136
225
  this.endTimeInput.value = '';
137
226
  this.endTimeInput.setCustomValidity('');
138
- const picker = this.container.querySelector('.timespan-picker');
139
- const durationEl = this.container.querySelector('.timespan-duration');
140
- const barFill = this.container.querySelector('.timespan-bar-fill');
141
- picker?.classList.remove('is-error');
142
- if (durationEl)
143
- durationEl.textContent = '';
144
- if (barFill) {
145
- barFill.style.left = '0';
146
- barFill.style.width = '0';
147
- }
227
+ this.pickerEl.classList.remove('is-error');
228
+ this.durationEl.textContent = '';
229
+ this.barFillEl.style.left = '0';
230
+ this.barFillEl.style.width = '0';
231
+ this.barFillEl.classList.remove('is-active');
148
232
  }
149
233
  isValid() {
150
234
  const { start, end } = this.getValue();
151
235
  return !!(start && end && start < end);
152
236
  }
153
237
  destroy() {
154
- this.startTimeInput.removeEventListener('change', this.handleStartChange);
155
- this.endTimeInput.removeEventListener('change', this.handleEndChange);
238
+ this.startTimeInput.removeEventListener('change', this.handleChange);
239
+ this.endTimeInput.removeEventListener('change', this.handleChange);
240
+ this.startHandleEl.removeEventListener('pointerdown', this.onStartHandleDown);
241
+ this.endHandleEl.removeEventListener('pointerdown', this.onEndHandleDown);
242
+ this.barFillEl.removeEventListener('pointerdown', this.onFillDown);
243
+ document.removeEventListener('pointermove', this.onPointerMove);
244
+ document.removeEventListener('pointerup', this.onPointerUp);
156
245
  }
157
246
  }
158
247
  export { TimeSpanPicker };
package/js/toast.d.ts CHANGED
@@ -18,7 +18,6 @@ declare class Toast {
18
18
  constructor(content: string, header?: string, type?: ToastType, closeable?: boolean);
19
19
  show(ms?: number): void;
20
20
  hide: () => void;
21
- private handleClose;
22
21
  private startTimer;
23
22
  private buildTemplate;
24
23
  }
package/js/toast.js CHANGED
@@ -11,17 +11,11 @@ class Toast {
11
11
  }
12
12
  this.toastElement?.classList.remove('show');
13
13
  setTimeout(() => {
14
- const closeButton = this.toastElement?.querySelector('.close');
15
- if (closeButton) {
16
- closeButton.removeEventListener('click', this.handleClose);
17
- }
14
+ this.toastElement?.querySelector('.close')?.removeEventListener('click', this.hide);
18
15
  this.toastElement?.remove();
19
16
  this.toastElement = null;
20
17
  }, 150);
21
18
  };
22
- this.handleClose = () => {
23
- this.hide();
24
- };
25
19
  if (typeof contentOrOptions === 'object') {
26
20
  this.content = contentOrOptions.content;
27
21
  this.header = contentOrOptions.header ?? '';
@@ -50,7 +44,7 @@ class Toast {
50
44
  this.toastElement?.classList.add('show');
51
45
  const closeButton = this.toastElement?.querySelector('.close');
52
46
  if (closeButton) {
53
- closeButton.addEventListener('click', this.handleClose);
47
+ closeButton.addEventListener('click', this.hide);
54
48
  }
55
49
  if (ms !== undefined && ms > 0) {
56
50
  this.startTimer(ms);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dodlhuat/basix",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Basix is intended as a starter for the rapid development of a design. Each design element can be added individually to include only the data required. It is using plain javascript / typescript and therefore is not dependent on any plugin.",
5
5
  "exports": {
6
6
  "./css/*": "./css/*",