js-superellipse 1.3.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 +110 -0
- package/dist/superellipse.js +4228 -0
- package/dist/superellipse.min.js +46 -0
- package/package.json +20 -0
|
@@ -0,0 +1,4228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @module sj-superellipse
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
* @author f4n70m
|
|
6
|
+
* @license MIT
|
|
7
|
+
*
|
|
8
|
+
* @description
|
|
9
|
+
* Библиотека для применения суперэллипсов к произвольным DOM-элементам.
|
|
10
|
+
* Позволяет плавно изменять форму углов от вогнутых прямоугольных (-2) до выгнутых прямоугольных (2),
|
|
11
|
+
* проходя через скос (0), круглые углы (±G) и классический суперэллипс (1).
|
|
12
|
+
*
|
|
13
|
+
* Особенности:
|
|
14
|
+
* - Два режима работы: `clip-path` (легковесный) и `svg-layer` (полнофункциональный с поддержкой границ и теней).
|
|
15
|
+
* - Автоматическое отслеживание изменений размеров, стилей, видимости и атрибутов элемента.
|
|
16
|
+
* - Поддержка `border-radius`, `border`, `box-shadow`, `background` в режиме `svg-layer`.
|
|
17
|
+
* - Несколько способов инициализации: через селектор, элемент, коллекцию или глобальную функцию.
|
|
18
|
+
* - Умное кэширование стилей для минимизации перерисовок.
|
|
19
|
+
*
|
|
20
|
+
* @typicalname superellipse
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Инициализация элемента с режимом svg-layer (по умолчанию)
|
|
24
|
+
* const element = document.querySelector('.my-element');
|
|
25
|
+
* const controller = element.superellipseInit({
|
|
26
|
+
* curveFactor: 1.2,
|
|
27
|
+
* precision: 3
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Изменение коэффициента кривизны
|
|
31
|
+
* controller.setCurveFactor(0.8);
|
|
32
|
+
*
|
|
33
|
+
* // Переключение режима
|
|
34
|
+
* controller.switchMode('clip-path');
|
|
35
|
+
*
|
|
36
|
+
* // Инициализация всех элементов с классом .rounded
|
|
37
|
+
* superellipseInit('.rounded', { mode: 'clip-path' });
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Генерация только SVG-пути без привязки к DOM
|
|
41
|
+
* import { jsse_generateSuperellipsePath } from 'js-superellipse';
|
|
42
|
+
* const path = jsse_generateSuperellipsePath(200, 150, 20, 1.5);
|
|
43
|
+
* document.querySelector('path').setAttribute('d', path);
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Глобальная функция для инициализации суперэллипса на одном или нескольких элементах.
|
|
48
|
+
* @function superellipseInit
|
|
49
|
+
* @memberof module:js-superellipse
|
|
50
|
+
* @param {string|Element|NodeList|Array<Element>} target - CSS-селектор, DOM-элемент или коллекция.
|
|
51
|
+
* @param {Object} [options] - Опции инициализации.
|
|
52
|
+
* @param {string} [options.mode='svg-layer'] - Режим: `'svg-layer'` (полная поддержка стилей) или `'clip-path'` (только обрезка).
|
|
53
|
+
* @param {number} [options.curveFactor] - Коэффициент кривизны (от -2 до 2). По умолчанию `(4/3)*(√2-1) ≈ 0.5523`.
|
|
54
|
+
* @param {number} [options.precision=2] - Количество знаков после запятой в координатах пути.
|
|
55
|
+
* @param {boolean} [options.force=false] - Принудительное пересоздание контроллера, если элемент уже инициализирован.
|
|
56
|
+
* @returns {SuperellipseController|Array<SuperellipseController>} Контроллер для одного элемента или массив контроллеров.
|
|
57
|
+
* @throws {Error} Если target не является селектором, элементом или коллекцией.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Класс контроллера, управляющего жизненным циклом суперэллипса для конкретного элемента.
|
|
62
|
+
* @class SuperellipseController
|
|
63
|
+
* @memberof module:js-superellipse
|
|
64
|
+
* @hideconstructor
|
|
65
|
+
* @example
|
|
66
|
+
* const controller = new SuperellipseController(element, options);
|
|
67
|
+
* // или через фабрику: element.superellipseInit(options)
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
(function (global, factory) {
|
|
71
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
72
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
73
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Superellipse = {}));
|
|
74
|
+
})(this, (function (exports) { 'use strict';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @file src/support.js
|
|
78
|
+
*
|
|
79
|
+
* @module sj-superellipse/support
|
|
80
|
+
* @since 1.0.0
|
|
81
|
+
* @author f4n70m
|
|
82
|
+
*
|
|
83
|
+
* @description
|
|
84
|
+
* Вспомогательные утилиты и инструменты отладки.
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Объект для проверки поддержки CSS-селекторов.
|
|
90
|
+
* @namespace jsse_css_selector
|
|
91
|
+
* @since 1.1.0
|
|
92
|
+
*/
|
|
93
|
+
const jsse_css_selector = {
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Кэш результатов проверки поддержки селекторов.
|
|
97
|
+
* @since 1.1.0
|
|
98
|
+
* @type {Object<string, boolean>}
|
|
99
|
+
*/
|
|
100
|
+
list : {},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Проверяет, поддерживает ли браузер указанный CSS-селектор.
|
|
104
|
+
* @since 1.1.0
|
|
105
|
+
* @param {string} selector - CSS-селектор для проверки (например, ':has(.a)').
|
|
106
|
+
* @returns {boolean} true, если селектор поддерживается, иначе false.
|
|
107
|
+
*/
|
|
108
|
+
isSupport(selector) {
|
|
109
|
+
if (this.list[selector] === undefined) {
|
|
110
|
+
try {
|
|
111
|
+
const value = `selector(${selector})`;
|
|
112
|
+
this.list[selector] = CSS.supports(value);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
this.list[selector] = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return this.list[selector];
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Объект для управления отладочным выводом в консоль.
|
|
124
|
+
* @namespace jsse_console
|
|
125
|
+
* @since 1.0.0
|
|
126
|
+
*/
|
|
127
|
+
const jsse_console = {
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Список DOM-элементов, для которых включена отладка.
|
|
131
|
+
* @type {Element[]}
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_list: [],
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Включает отладку для указанного элемента.
|
|
138
|
+
* @since 1.0.0
|
|
139
|
+
* @param {Element} element - DOM-элемент.
|
|
140
|
+
* @returns {void}
|
|
141
|
+
*/
|
|
142
|
+
set(element) {
|
|
143
|
+
this._list.push(element);
|
|
144
|
+
// this.debug('set', {element});
|
|
145
|
+
// console.log(this._list);
|
|
146
|
+
// console.debug(`[DEBUG]`, {src:'jsse_console::set', element});
|
|
147
|
+
console.debug('[JSSE]', '[DEBUG]', true, '\n\t', {element:element});
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Выводит отладочное сообщение в консоль (если отладка включена для элемента или сообщение глобальное).
|
|
152
|
+
* @since 1.0.0
|
|
153
|
+
* @param {Object} options - Опции.
|
|
154
|
+
* @param {Element} [options.element] - Элемент, для которого проверяется включение отладки.
|
|
155
|
+
* @param {string} [options.label='DEBUG'] - Метка для вывода.
|
|
156
|
+
* @param {...any} values - Значения для вывода.
|
|
157
|
+
* @returns {void}
|
|
158
|
+
*/
|
|
159
|
+
debug(options, ...values) {
|
|
160
|
+
if (options.element) {
|
|
161
|
+
if(this._list.includes(options.element)) {
|
|
162
|
+
console.debug('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values, '\n\t', {element:options.element});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.debug('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Выводит предупреждение в консоль (если отладка включена для элемента или сообщение глобальное).
|
|
172
|
+
* @since 1.0.0
|
|
173
|
+
* @param {Object} options - Опции.
|
|
174
|
+
* @param {Element} [options.element] - Элемент, для которого проверяется включение отладки.
|
|
175
|
+
* @param {string} [options.label='DEBUG'] - Метка для вывода.
|
|
176
|
+
* @param {...any} values - Значения для вывода.
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
warn(options, ...values) {
|
|
180
|
+
if (options.element) {
|
|
181
|
+
if(this._list.includes(options.element)) {
|
|
182
|
+
console.warn('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values, '\n\t', {element:options.element});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.warn('[JSSE]', `[${options?.label??'DEBUG'}]`, ...values);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @file src/support.js
|
|
193
|
+
*
|
|
194
|
+
* @module sj-superellipse/css-parser
|
|
195
|
+
* @since 1.1.0
|
|
196
|
+
* @author f4n70m
|
|
197
|
+
*
|
|
198
|
+
* @description
|
|
199
|
+
* Модуль парсинга css стилей страницы.
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Представляет фрагмент CSS-селектора (часть между комбинаторами).
|
|
206
|
+
* @class StylesheetParserFragment
|
|
207
|
+
* @since 1.1.0
|
|
208
|
+
*/
|
|
209
|
+
class StylesheetParserFragment {
|
|
210
|
+
_combinator;
|
|
211
|
+
_full;
|
|
212
|
+
_clean;
|
|
213
|
+
_pseudo;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {Object} options - Параметры фрагмента.
|
|
217
|
+
* @param {string} options.combinator - Комбинатор (пробел, '>', '+', '~').
|
|
218
|
+
* @param {string} options.full - Полный текст фрагмента.
|
|
219
|
+
* @param {string} options.clean - Очищенный текст (без псевдоклассов).
|
|
220
|
+
* @param {string[]} options.pseudo - Список псевдоклассов/псевдоэлементов.
|
|
221
|
+
*/
|
|
222
|
+
constructor(options) {
|
|
223
|
+
this._combinator = options.combinator;
|
|
224
|
+
const isRoot = options.full === ':root';
|
|
225
|
+
this._clean = isRoot||options.clean?options.clean:`*`;
|
|
226
|
+
this._full = isRoot||options.clean?options.full:`*${options.full}`;
|
|
227
|
+
this._pseudo = [...new Set(options.pseudo)];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Возвращает комбинатор.
|
|
232
|
+
* @since 1.1.0
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
getCombinator() {
|
|
236
|
+
return this._combinator;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Возвращает полный текст фрагмента.
|
|
241
|
+
* @since 1.1.0
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
getFull() {
|
|
245
|
+
return this._full;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Возвращает очищенный текст фрагмента.
|
|
250
|
+
* @since 1.1.0
|
|
251
|
+
* @returns {string}
|
|
252
|
+
*/
|
|
253
|
+
getClean() { return this._clean; };
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Возвращает список псевдоклассов/псевдоэлементов.
|
|
257
|
+
* @since 1.1.0
|
|
258
|
+
* @returns {string[]}
|
|
259
|
+
*/
|
|
260
|
+
getPseudoList() { return this.pseudo; };
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Проверяет наличие конкретного псевдокласса.
|
|
264
|
+
* @since 1.1.0
|
|
265
|
+
* @param {string} pseudo - Псевдокласс (например, ':hover').
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
hasPseudo(pseudo) {
|
|
269
|
+
return this._pseudo.includes(pseudo);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Проверяет наличие псевдокласса `:hover`.
|
|
274
|
+
* @since 1.1.0
|
|
275
|
+
* @returns {boolean}
|
|
276
|
+
*/
|
|
277
|
+
hasHover() {
|
|
278
|
+
return this.hasPseudo(':hover');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Список фрагментов селектора (массив с валидацией).
|
|
285
|
+
* @class StylesheetParserFragmentList
|
|
286
|
+
* @extends Array
|
|
287
|
+
* @since 1.1.0
|
|
288
|
+
*/
|
|
289
|
+
class StylesheetParserFragmentList extends Array {
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* =============================================================
|
|
294
|
+
* ARRAY
|
|
295
|
+
* =============================================================
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Проверяет, является ли элемент валидным фрагментом.
|
|
301
|
+
* @since 1.1.0
|
|
302
|
+
* @static
|
|
303
|
+
* @private
|
|
304
|
+
* @param {any} item - Проверяемый элемент.
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
*/
|
|
307
|
+
static #isValid(item) {
|
|
308
|
+
return item instanceof StylesheetParserFragment;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Добавляет валидные фрагменты в конец массива.
|
|
313
|
+
* @override
|
|
314
|
+
* @param {...StylesheetParserFragment} items - Фрагменты.
|
|
315
|
+
* @returns {number}
|
|
316
|
+
*/
|
|
317
|
+
push(...items) {
|
|
318
|
+
const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
|
|
319
|
+
return super.push(...valid);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Добавляет валидные фрагменты в начало массива.
|
|
324
|
+
* @override
|
|
325
|
+
* @param {...StylesheetParserFragment} items - Фрагменты.
|
|
326
|
+
* @returns {number}
|
|
327
|
+
*/
|
|
328
|
+
unshift(...items) {
|
|
329
|
+
const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
|
|
330
|
+
return super.unshift(...valid);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Изменяет содержимое массива, удаляя или заменяя элементы.
|
|
335
|
+
* @override
|
|
336
|
+
* @param {number} start - Индекс начала.
|
|
337
|
+
* @param {number} deleteCount - Количество удаляемых элементов.
|
|
338
|
+
* @param {...StylesheetParserFragment} items - Добавляемые фрагменты.
|
|
339
|
+
* @returns {StylesheetParserFragment[]}
|
|
340
|
+
*/
|
|
341
|
+
splice(start, deleteCount, ...items) {
|
|
342
|
+
const valid = items.filter(item => StylesheetParserFragmentList.#isValid(item));
|
|
343
|
+
return super.splice(start, deleteCount, ...valid);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Все остальные методы (forEach, map, filter, find, pop, shift и т.д.)
|
|
347
|
+
// работают автоматически, так как они наследуются от Array
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* =============================================================
|
|
352
|
+
* PUBLIC
|
|
353
|
+
* =============================================================
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Возвращает последний фрагмент (целевой).
|
|
359
|
+
* @since 1.1.0
|
|
360
|
+
* @returns {StylesheetParserFragment}
|
|
361
|
+
*/
|
|
362
|
+
getTarget() {
|
|
363
|
+
return this[this.length-1];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Возвращает индексы фрагментов, содержащих `:hover`.
|
|
368
|
+
* @since 1.1.0
|
|
369
|
+
* @returns {number[]}
|
|
370
|
+
*/
|
|
371
|
+
getTriggerIndexList() {
|
|
372
|
+
return this.reduce((indexes, element, index) => {
|
|
373
|
+
if (element.hasHover()) {
|
|
374
|
+
indexes.push(index);
|
|
375
|
+
}
|
|
376
|
+
return indexes;
|
|
377
|
+
}, []);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Проверяет, есть ли хотя бы один фрагмент с `:hover`.
|
|
382
|
+
* @since 1.1.0
|
|
383
|
+
* @returns {boolean}
|
|
384
|
+
*/
|
|
385
|
+
hasTrigger() {
|
|
386
|
+
return this.getTriggerIndexList().length > 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Проверяет, содержит ли целевой фрагмент `:hover`.
|
|
391
|
+
* @since 1.1.0
|
|
392
|
+
* @returns {boolean}
|
|
393
|
+
*/
|
|
394
|
+
targetIsTriggered() {
|
|
395
|
+
return this.getTarget().hasHover();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* =============================================================
|
|
401
|
+
* PRIVATE
|
|
402
|
+
* =============================================================
|
|
403
|
+
*/
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Представляет CSS-правило (селектор + стили).
|
|
409
|
+
* @class StylesheetParserSelector
|
|
410
|
+
* @since 1.1.0
|
|
411
|
+
*/
|
|
412
|
+
class StylesheetParserSelector {
|
|
413
|
+
|
|
414
|
+
_media;
|
|
415
|
+
_selector;
|
|
416
|
+
_fragments;
|
|
417
|
+
|
|
418
|
+
_ruleStyle;
|
|
419
|
+
_styles;
|
|
420
|
+
|
|
421
|
+
_triggerFragments;
|
|
422
|
+
_triggerIndexList;
|
|
423
|
+
_triggerParts;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {string} selector - Текст селектора.
|
|
427
|
+
* @param {CSSStyleDeclaration} ruleStyle - Стили правила.
|
|
428
|
+
* @param {string|false} media - Медиа-выражение (если правило внутри @media).
|
|
429
|
+
*/
|
|
430
|
+
constructor(selector, ruleStyle, media) {
|
|
431
|
+
/** @type {string|false} */
|
|
432
|
+
this._media = media;
|
|
433
|
+
/** @type {string} */
|
|
434
|
+
this._selector = selector;
|
|
435
|
+
/** @type {StylesheetParserFragmentList|null} */
|
|
436
|
+
this._fragments = null;
|
|
437
|
+
/** @type {CSSStyleDeclaration} */
|
|
438
|
+
this._ruleStyle = ruleStyle;
|
|
439
|
+
/** @type {Object<string, string>|null} */
|
|
440
|
+
this._styles = null;
|
|
441
|
+
/** @type {StylesheetParserFragmentList|null} */
|
|
442
|
+
this._triggerFragments = null;
|
|
443
|
+
/** @type {number[]|null} */
|
|
444
|
+
this._triggerIndexList = null;
|
|
445
|
+
/** @type {Array|null} */
|
|
446
|
+
this._triggerParts = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* =============================================================
|
|
451
|
+
* PUBLIC
|
|
452
|
+
* =============================================================
|
|
453
|
+
*/
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Возвращает текст селектора.
|
|
458
|
+
* @since 1.1.0
|
|
459
|
+
* @returns {string}
|
|
460
|
+
*/
|
|
461
|
+
getSelector() {
|
|
462
|
+
return this._selector;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Возвращает распарсенные стили правила.
|
|
467
|
+
* @since 1.1.0
|
|
468
|
+
* @returns {Object<string, string>}
|
|
469
|
+
*/
|
|
470
|
+
getStyles() {
|
|
471
|
+
if (this._styles === null) {
|
|
472
|
+
this._styles = this._parseStyles();
|
|
473
|
+
}
|
|
474
|
+
return this._styles;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Возвращает список фрагментов селектора.
|
|
479
|
+
* @since 1.1.0
|
|
480
|
+
* @returns {StylesheetParserFragmentList}
|
|
481
|
+
*/
|
|
482
|
+
getFragments() {
|
|
483
|
+
if (this._fragments === null) {
|
|
484
|
+
this._fragments = this._getSelectorFragments(this._selector);
|
|
485
|
+
}
|
|
486
|
+
return this._fragments;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Проверяет, соответствует ли текущее медиа-правило.
|
|
491
|
+
* @since 1.1.0
|
|
492
|
+
* @returns {boolean}
|
|
493
|
+
*/
|
|
494
|
+
matchMedia() {
|
|
495
|
+
return !this._media || window.matchMedia(this._media).matches;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Возвращает части селектора, участвующие в hover-триггерах.
|
|
500
|
+
* @since 1.1.0
|
|
501
|
+
* @returns {Array<{parent: string, neighbor: Object|null, child: string}>}
|
|
502
|
+
*/
|
|
503
|
+
getTriggerParts() {
|
|
504
|
+
if (this._triggerParts === null) {
|
|
505
|
+
this._triggerParts = [];
|
|
506
|
+
const fragments = this.getFragments();
|
|
507
|
+
const targetIndexList = fragments.getTriggerIndexList();
|
|
508
|
+
for(const targetIndex of targetIndexList) {
|
|
509
|
+
const parts = {parent:'',neighbor:null,child:null};
|
|
510
|
+
for (let i = 0; i<fragments.length; i++) {
|
|
511
|
+
const fragment = fragments[i];
|
|
512
|
+
if (i <= targetIndex) {
|
|
513
|
+
parts.parent += fragment.getCombinator() + fragment.getClean();
|
|
514
|
+
|
|
515
|
+
if (i + 1 < fragments.length) {
|
|
516
|
+
const nextFragment = fragments[i+1];
|
|
517
|
+
const nextCombinator = nextFragment.getCombinator();
|
|
518
|
+
if ([' + ', ' ~ '].includes(nextCombinator)) {
|
|
519
|
+
parts.neighbor = {
|
|
520
|
+
combinator: nextCombinator,
|
|
521
|
+
clean : nextFragment.getClean()
|
|
522
|
+
};
|
|
523
|
+
i++;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
if (!parts.child) parts.child = '';
|
|
528
|
+
parts.child += fragment.getCombinator() + fragment.getClean();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
this._triggerParts.push(parts);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return this._triggerParts;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Возвращает фрагменты, содержащие `:hover`.
|
|
539
|
+
* @since 1.1.0
|
|
540
|
+
* @returns {StylesheetParserFragmentList}
|
|
541
|
+
*/
|
|
542
|
+
getTriggerFragments() {
|
|
543
|
+
if (this._triggerFragments === null) {
|
|
544
|
+
this._triggerFragments = new StylesheetParserFragmentList();
|
|
545
|
+
const fragments = this.getFragments();
|
|
546
|
+
const indexList = fragments.getTriggerIndexList();
|
|
547
|
+
for(const index of indexList) {
|
|
548
|
+
const fragment = fragments[index];
|
|
549
|
+
this._triggerFragments.push(fragment);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return this._triggerFragments;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* =============================================================
|
|
559
|
+
* PRIVATE
|
|
560
|
+
* =============================================================
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Парсит CSS-стили из cssText.
|
|
565
|
+
* @private
|
|
566
|
+
* @returns {Object<string, string>}
|
|
567
|
+
*/
|
|
568
|
+
_parseStyles() {
|
|
569
|
+
const styles = {};
|
|
570
|
+
if (this._ruleStyle.cssText) {
|
|
571
|
+
const declarations = this._ruleStyle.cssText.split(';');
|
|
572
|
+
for (const decl of declarations) {
|
|
573
|
+
const colonIndex = decl.indexOf(':');
|
|
574
|
+
if (colonIndex > 0) {
|
|
575
|
+
const prop = decl.substring(0, colonIndex).trim();
|
|
576
|
+
const value = decl.substring(colonIndex + 1).trim();
|
|
577
|
+
if (prop && value) {
|
|
578
|
+
styles[prop] = value;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return styles;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Разбивает селектор на фрагменты.
|
|
588
|
+
* @private
|
|
589
|
+
* @param {string} selector - Текст селектора.
|
|
590
|
+
* @returns {StylesheetParserFragmentList}
|
|
591
|
+
*/
|
|
592
|
+
_getSelectorFragments(selector) {
|
|
593
|
+
const s = selector;
|
|
594
|
+
const result = new StylesheetParserFragmentList();
|
|
595
|
+
let options = {
|
|
596
|
+
combinator : '',
|
|
597
|
+
full : '',
|
|
598
|
+
clean : '',
|
|
599
|
+
pseudo: []
|
|
600
|
+
};
|
|
601
|
+
let pseudo = '';
|
|
602
|
+
let i = 0;
|
|
603
|
+
while (i < s.length) {
|
|
604
|
+
if (s[i] === ':') {
|
|
605
|
+
/** завершение записи текущего фрагмента перед псевдоэлементом **/
|
|
606
|
+
if (i > 0 && s[i + 1] === ':') {
|
|
607
|
+
|
|
608
|
+
if (options.combinator === '') options.combinator = ' ';
|
|
609
|
+
|
|
610
|
+
result.push(new StylesheetParserFragment(options));
|
|
611
|
+
options = {
|
|
612
|
+
combinator : '',
|
|
613
|
+
full : '',
|
|
614
|
+
clean : '',
|
|
615
|
+
pseudo: []
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
/** Псевдоэлемент (два двоеточия) **/
|
|
619
|
+
if (s[i + 1] === ':' || (i > 0 && s[i - 1] === ':')) {
|
|
620
|
+
options.full += s[i];
|
|
621
|
+
options.clean += s[i];
|
|
622
|
+
i++;
|
|
623
|
+
}
|
|
624
|
+
/** Псевдокласс **/
|
|
625
|
+
else {
|
|
626
|
+
|
|
627
|
+
pseudo += s[i];
|
|
628
|
+
let j = i + 1;
|
|
629
|
+
// pseudo += s[i];
|
|
630
|
+
while (j < s.length && /[\w-]/.test(s[j])) {
|
|
631
|
+
pseudo += s[j];
|
|
632
|
+
j++;
|
|
633
|
+
}
|
|
634
|
+
// если есть аргументы в скобках
|
|
635
|
+
if (s[j] === '(') {
|
|
636
|
+
pseudo += s[j];
|
|
637
|
+
let depth = 1, k = j + 1;
|
|
638
|
+
while (k < selector.length && depth) {
|
|
639
|
+
if (selector[k] === '(') depth++;
|
|
640
|
+
else if (selector[k] === ')') depth--;
|
|
641
|
+
pseudo += s[k];
|
|
642
|
+
k++;
|
|
643
|
+
}
|
|
644
|
+
options.pseudo.push(pseudo);
|
|
645
|
+
options.full += pseudo;
|
|
646
|
+
pseudo = '';
|
|
647
|
+
i = k;
|
|
648
|
+
}
|
|
649
|
+
// если
|
|
650
|
+
else {
|
|
651
|
+
options.pseudo.push(pseudo);
|
|
652
|
+
options.full += pseudo;
|
|
653
|
+
pseudo = '';
|
|
654
|
+
i = j;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} else {
|
|
658
|
+
/** проверка combinator **/
|
|
659
|
+
if (s[i] === ' ') {
|
|
660
|
+
|
|
661
|
+
result.push(new StylesheetParserFragment(options));
|
|
662
|
+
|
|
663
|
+
let combinator = ' ';
|
|
664
|
+
if (/[\>\~\+]/.test(s[i+1]) && s[i + 2] === ' ') {
|
|
665
|
+
combinator = s[i]+s[i+1]+s[i+2];
|
|
666
|
+
i += 2;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
options = {
|
|
670
|
+
combinator : combinator,
|
|
671
|
+
full : '',
|
|
672
|
+
clean : '',
|
|
673
|
+
pseudo: []
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
i++;
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
options.full += s[i];
|
|
680
|
+
options.clean += s[i];
|
|
681
|
+
i++;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
result.push(new StylesheetParserFragment(options));
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Список CSS-селекторов (массив с валидацией).
|
|
693
|
+
* @class StylesheetParserSelectorList
|
|
694
|
+
* @extends Array
|
|
695
|
+
* @since 1.1.0
|
|
696
|
+
*/
|
|
697
|
+
class StylesheetParserSelectorList extends Array {
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* =============================================================
|
|
702
|
+
* ARRAY
|
|
703
|
+
* =============================================================
|
|
704
|
+
*/
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Проверяет, является ли элемент валидным селектором.
|
|
709
|
+
* @since 1.1.0
|
|
710
|
+
* @static
|
|
711
|
+
* @private
|
|
712
|
+
* @param {any} item - Проверяемый элемент.
|
|
713
|
+
* @returns {boolean}
|
|
714
|
+
*/
|
|
715
|
+
static #isValid(item) {
|
|
716
|
+
return item instanceof StylesheetParserSelector;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Добавляет валидные селекторы в конец массива.
|
|
721
|
+
* @since 1.1.0
|
|
722
|
+
* @override
|
|
723
|
+
* @param {...StylesheetParserSelector} items - Селекторы.
|
|
724
|
+
* @returns {number}
|
|
725
|
+
*/
|
|
726
|
+
push(...items) {
|
|
727
|
+
const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
|
|
728
|
+
return super.push(...valid);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Добавляет валидные селекторы в начало массива.
|
|
733
|
+
* @since 1.1.0
|
|
734
|
+
* @override
|
|
735
|
+
* @param {...StylesheetParserSelector} items - Селекторы.
|
|
736
|
+
* @returns {number}
|
|
737
|
+
*/
|
|
738
|
+
unshift(...items) {
|
|
739
|
+
const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
|
|
740
|
+
return super.unshift(...valid);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Изменяет содержимое массива, удаляя или заменяя элементы.
|
|
745
|
+
* @since 1.1.0
|
|
746
|
+
* @override
|
|
747
|
+
* @param {number} start - Индекс начала.
|
|
748
|
+
* @param {number} deleteCount - Количество удаляемых элементов.
|
|
749
|
+
* @param {...StylesheetParserSelector} items - Добавляемые селекторы.
|
|
750
|
+
* @returns {StylesheetParserSelector[]}
|
|
751
|
+
*/
|
|
752
|
+
splice(start, deleteCount, ...items) {
|
|
753
|
+
const valid = items.filter(item => StylesheetParserSelectorList.#isValid(item));
|
|
754
|
+
return super.splice(start, deleteCount, ...valid);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Все остальные методы (forEach, map, filter, find, pop, shift и т.д.)
|
|
758
|
+
// работают автоматически, так как они наследуются от Array
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* =============================================================
|
|
763
|
+
* PUBLIC
|
|
764
|
+
* =============================================================
|
|
765
|
+
*/
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Возвращает селекторы, содержащие `:hover`.
|
|
770
|
+
* @since 1.1.0
|
|
771
|
+
* @returns {StylesheetParserSelector[]}
|
|
772
|
+
*/
|
|
773
|
+
getSelectorsWithHover() {
|
|
774
|
+
return this.filter(item => item.getFragments().hasTrigger());
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* =============================================================
|
|
780
|
+
* PRIVATE
|
|
781
|
+
* =============================================================
|
|
782
|
+
*/
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Парсер таблиц стилей. Извлекает CSS-правила, разбирает селекторы и предоставляет доступ к ним.
|
|
788
|
+
* @class StylesheetParser
|
|
789
|
+
* @since 1.1.0
|
|
790
|
+
*/
|
|
791
|
+
class StylesheetParser {
|
|
792
|
+
_selectors;
|
|
793
|
+
_isParsed;
|
|
794
|
+
|
|
795
|
+
constructor() {
|
|
796
|
+
/** @type {StylesheetParserSelectorList|null} */
|
|
797
|
+
this._selectors = null;
|
|
798
|
+
/** @type {boolean} */
|
|
799
|
+
this._isParsed = false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* =============================================================
|
|
804
|
+
* PUBLIC
|
|
805
|
+
* =============================================================
|
|
806
|
+
*/
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Возвращает список всех селекторов.
|
|
810
|
+
* @since 1.1.0
|
|
811
|
+
* @returns {StylesheetParserSelectorList}
|
|
812
|
+
*/
|
|
813
|
+
getSelectors() {
|
|
814
|
+
this._ensureParsed();
|
|
815
|
+
return this._selectors;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Возвращает селекторы, содержащие `:hover`.
|
|
820
|
+
* @since 1.1.0
|
|
821
|
+
* @returns {StylesheetParserSelector[]}
|
|
822
|
+
*/
|
|
823
|
+
getSelectorsHasHover() {
|
|
824
|
+
this._ensureParsed();
|
|
825
|
+
return this._selectors.getSelectorsWithHover();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Возвращает селекторы, которые соответствуют элементу и (опционально) содержат `:hover`.
|
|
830
|
+
* @since 1.1.0
|
|
831
|
+
* @param {Element} element - Целевой элемент.
|
|
832
|
+
* @param {Object} [options] - Опции.
|
|
833
|
+
* @param {boolean} [options.selectorHasHover] - Если true, возвращать только селекторы с `:hover`.
|
|
834
|
+
* @returns {StylesheetParserSelector[]}
|
|
835
|
+
*/
|
|
836
|
+
getTargetSelectors(element, options={}) {
|
|
837
|
+
this._ensureParsed();
|
|
838
|
+
const targetList = this._createList();
|
|
839
|
+
const parserList = this.getSelectors();
|
|
840
|
+
for(const selector of parserList) {
|
|
841
|
+
const fragments = selector.getFragments();
|
|
842
|
+
const clean = fragments.getTarget().getClean();
|
|
843
|
+
if (!clean) continue;
|
|
844
|
+
const isMatch = element.matches(clean);
|
|
845
|
+
fragments.hasTrigger();
|
|
846
|
+
if (
|
|
847
|
+
isMatch &&
|
|
848
|
+
( !options?.selectorHasHover || options.selectorHasHover && fragments.hasTrigger() )
|
|
849
|
+
) {
|
|
850
|
+
targetList.push(selector);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return targetList;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Сбрасывает состояние парсера (принудительный перепарсинг при следующем вызове).
|
|
858
|
+
* @since 1.1.0
|
|
859
|
+
* @returns {void}
|
|
860
|
+
*/
|
|
861
|
+
reset() {
|
|
862
|
+
this._selectors = null;
|
|
863
|
+
this._isParsed = false;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* =============================================================
|
|
868
|
+
* PRIVATE
|
|
869
|
+
* =============================================================
|
|
870
|
+
*/
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Гарантирует, что парсинг выполнен.
|
|
875
|
+
* @private
|
|
876
|
+
* @returns {void}
|
|
877
|
+
*/
|
|
878
|
+
_ensureParsed() {
|
|
879
|
+
if (this._isParsed) return;
|
|
880
|
+
this._init();
|
|
881
|
+
this._isParsed = true;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Инициализирует парсер: создаёт список и парсит CSS-правила.
|
|
886
|
+
* @private
|
|
887
|
+
* @returns {void}
|
|
888
|
+
*/
|
|
889
|
+
_init() {
|
|
890
|
+
this._selectors = this._createList();
|
|
891
|
+
this._parseCssRules();
|
|
892
|
+
jsse_console.debug({label:'STYLESHEET'}, '[LOADED]');
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Создаёт пустой список селекторов.
|
|
897
|
+
* @private
|
|
898
|
+
* @returns {StylesheetParserSelectorList}
|
|
899
|
+
*/
|
|
900
|
+
_createList() {
|
|
901
|
+
return new StylesheetParserSelectorList();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Парсит все CSS-правила из document.styleSheets.
|
|
906
|
+
* @private
|
|
907
|
+
* @returns {void}
|
|
908
|
+
*/
|
|
909
|
+
_parseCssRules() {
|
|
910
|
+
for (const styleSheet of document.styleSheets) {
|
|
911
|
+
try {
|
|
912
|
+
const rules = styleSheet.cssRules || styleSheet.rules;
|
|
913
|
+
if (!rules) continue;
|
|
914
|
+
|
|
915
|
+
for (const rule of rules) {
|
|
916
|
+
if (rule.type === CSSRule.STYLE_RULE) {
|
|
917
|
+
this._selectors.push(...this._getParsedCssRule(rule));
|
|
918
|
+
}
|
|
919
|
+
else if (rule.type === CSSRule.MEDIA_RULE) {
|
|
920
|
+
try {
|
|
921
|
+
for (const subRule of rule.cssRules) {
|
|
922
|
+
this._selectors.push(...this._getParsedCssRule(subRule, rule.media.mediaText));
|
|
923
|
+
}
|
|
924
|
+
} catch (e) {
|
|
925
|
+
jsse_console.warn({label:'STYLESHEET'}, `Error processing @media rules from ${styleSheet.href || 'inline'}:`, e.message);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
// Можно добавить другие типы правил (CSSRule.IMPORT_RULE, CSSRule.SUPPORTS_RULE и т.д.)
|
|
929
|
+
}
|
|
930
|
+
} catch (e) {
|
|
931
|
+
if (e.name === 'SecurityError') {
|
|
932
|
+
jsse_console.warn(
|
|
933
|
+
{label:'STYLESHEET'},
|
|
934
|
+
`Cannot access stylesheet rules:`,
|
|
935
|
+
`\n${styleSheet.href || 'inline / blob'}.`,
|
|
936
|
+
`\nCause: CORS or file:// protocol.`,
|
|
937
|
+
`\nTo fix this, use a local server (http://) or add the crossorigin attribute.`
|
|
938
|
+
);
|
|
939
|
+
} else if (e.name === 'InvalidAccessError') {
|
|
940
|
+
jsse_console.warn({label:'STYLESHEET'}, `The stylesheet ${styleSheet.href || 'inline'} has not yet loaded or has invalid access.`);
|
|
941
|
+
} else {
|
|
942
|
+
jsse_console.warn({label:'STYLESHEET'}, `Unknown error reading ${styleSheet.href || 'inline'}:`, e.message);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Преобразует CSSRule в массив селекторов.
|
|
950
|
+
* @private
|
|
951
|
+
* @param {CSSRule} rule - CSS-правило.
|
|
952
|
+
* @param {string|false} [media=false] - Медиа-выражение (если правило внутри @media).
|
|
953
|
+
* @returns {StylesheetParserSelector[]}
|
|
954
|
+
*/
|
|
955
|
+
_getParsedCssRule(rule, media=false) {
|
|
956
|
+
const result = [];
|
|
957
|
+
const selectorGroup = rule.selectorText;
|
|
958
|
+
const selectorList = this._splitSelectorGroup(selectorGroup);
|
|
959
|
+
const uniqueSelectorList = [...new Set(selectorList)];
|
|
960
|
+
for (const selector of uniqueSelectorList) {
|
|
961
|
+
result.push( new StylesheetParserSelector(selector, rule.style, media) );
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Разбивает группу селекторов по запятым, игнорируя запятые внутри скобок.
|
|
969
|
+
* @private
|
|
970
|
+
* @param {string} selectorText - Текст группы селекторов.
|
|
971
|
+
* @returns {string[]}
|
|
972
|
+
* @example
|
|
973
|
+
* _splitSelectorGroup(":not(.a, .b), .c") → [":not(.a, .b)", ".c"]
|
|
974
|
+
*/
|
|
975
|
+
_splitSelectorGroup(selectorText) {
|
|
976
|
+
const result = [];
|
|
977
|
+
let current = '';
|
|
978
|
+
let depth = 0; // глубина вложенности в круглые скобки
|
|
979
|
+
let inString = false; // для простоты не поддерживаем строки (в CSS их почти нет в селекторах)
|
|
980
|
+
let escape = false;
|
|
981
|
+
|
|
982
|
+
for (let i = 0; i < selectorText.length; i++) {
|
|
983
|
+
const ch = selectorText[i];
|
|
984
|
+
|
|
985
|
+
if (escape) {
|
|
986
|
+
current += ch;
|
|
987
|
+
escape = false;
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (ch === '\\') {
|
|
992
|
+
escape = true;
|
|
993
|
+
current += ch;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (ch === '"' || ch === "'") {
|
|
998
|
+
inString = !inString;
|
|
999
|
+
current += ch;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (!inString) {
|
|
1004
|
+
if (ch === '(') {
|
|
1005
|
+
depth++;
|
|
1006
|
+
} else if (ch === ')') {
|
|
1007
|
+
depth--;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (ch === ',' && depth === 0 && !inString) {
|
|
1012
|
+
// Запятая на нулевом уровне – разделитель
|
|
1013
|
+
result.push(current.trim());
|
|
1014
|
+
current = '';
|
|
1015
|
+
} else {
|
|
1016
|
+
current += ch;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (current.trim() !== '') {
|
|
1021
|
+
result.push(current.trim());
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return result;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* @file src/global-cache.js
|
|
1030
|
+
*
|
|
1031
|
+
* @module sj-superellipse/global-cache
|
|
1032
|
+
* @since 1.0.0
|
|
1033
|
+
* @author f4n70m
|
|
1034
|
+
*
|
|
1035
|
+
* @description
|
|
1036
|
+
* Глобальные хранилища для кэширования данных между экземплярами контроллеров и режимов.
|
|
1037
|
+
*/
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Карта для хранения контроллеров суперэллипса, связанных с DOM-элементами.
|
|
1043
|
+
* @since 1.0.0
|
|
1044
|
+
* @type {WeakMap<Element, import('./controller.js').SuperellipseController>}
|
|
1045
|
+
*/
|
|
1046
|
+
const jsse_controllers = new WeakMap();
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Экземпляр парсера таблиц стилей для анализа CSS-правил.
|
|
1051
|
+
* @since 1.1.0
|
|
1052
|
+
* @type {import('./stylesheet-parser.js').StylesheetParser}
|
|
1053
|
+
*/
|
|
1054
|
+
const jsse_stylesheet = new StylesheetParser();
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* WeakMap для кэширования стилей элементов.
|
|
1059
|
+
* @since 1.0.0
|
|
1060
|
+
* @type {WeakMap<Element, Object>}
|
|
1061
|
+
*/
|
|
1062
|
+
const jsse_styles = new WeakMap();
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Объект для хранения глобальных CSS-правил режимов.
|
|
1067
|
+
* @since 1.0.0
|
|
1068
|
+
* @namespace jsse_reset_css
|
|
1069
|
+
*/
|
|
1070
|
+
const jsse_reset_css = {
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Внутреннее хранилище элементов `<style>` для каждого режима.
|
|
1074
|
+
* @type {Object<string, {element: HTMLStyleElement, count: number}>}
|
|
1075
|
+
*/
|
|
1076
|
+
_list: {},
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Возвращает запись для заданного ключа.
|
|
1080
|
+
* @since 1.0.0
|
|
1081
|
+
* @param {string} key - Идентификатор режима (например, 'svg-layer').
|
|
1082
|
+
* @returns {{element: HTMLStyleElement, count: number} | undefined}
|
|
1083
|
+
*/
|
|
1084
|
+
get(key) {
|
|
1085
|
+
return this._list[key];
|
|
1086
|
+
},
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* Сохраняет элемент `<style>` для режима, удаляя предыдущий при необходимости.
|
|
1090
|
+
* @since 1.0.0
|
|
1091
|
+
* @param {string} key - Идентификатор режима.
|
|
1092
|
+
* @param {HTMLStyleElement} el - Элемент стилей для добавления в `<head>`.
|
|
1093
|
+
* @returns {void}
|
|
1094
|
+
*/
|
|
1095
|
+
set(key, el) {
|
|
1096
|
+
if (this.has(key)) {
|
|
1097
|
+
this.unset(key);
|
|
1098
|
+
}
|
|
1099
|
+
this._list[key] = {
|
|
1100
|
+
element: el,
|
|
1101
|
+
count: 1
|
|
1102
|
+
};
|
|
1103
|
+
/** Добавить элемент в конец <head> **/
|
|
1104
|
+
document.head.appendChild(el);
|
|
1105
|
+
},
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Удаляет запись и соответствующий элемент `<style>` из DOM.
|
|
1109
|
+
* @since 1.0.0
|
|
1110
|
+
* @param {string} key - Идентификатор режима.
|
|
1111
|
+
* @returns {void}
|
|
1112
|
+
*/
|
|
1113
|
+
unset(key) {
|
|
1114
|
+
/** Удалить элемент **/
|
|
1115
|
+
this._list[key].element.remove();
|
|
1116
|
+
delete this._list[key];
|
|
1117
|
+
},
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Проверяет существование записи для указанного ключа.
|
|
1121
|
+
* @since 1.0.0
|
|
1122
|
+
* @param {string} key - Идентификатор режима.
|
|
1123
|
+
* @returns {boolean}
|
|
1124
|
+
*/
|
|
1125
|
+
has(key) {
|
|
1126
|
+
return this._list[key] !== undefined;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Счётчик для генерации уникальных идентификаторов контроллеров.
|
|
1132
|
+
* @since 1.0.0
|
|
1133
|
+
* @namespace jsse_counter
|
|
1134
|
+
*/
|
|
1135
|
+
const jsse_counter = {
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Текущее значение счётчика.
|
|
1139
|
+
* @type {number}
|
|
1140
|
+
*/
|
|
1141
|
+
_value: 0,
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Увеличивает счётчик на 1.
|
|
1145
|
+
* @returns {void}
|
|
1146
|
+
*/
|
|
1147
|
+
increment() { this._value++; },
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Уменьшает счётчик на 1.
|
|
1151
|
+
* @returns {void}
|
|
1152
|
+
*/
|
|
1153
|
+
decrement() { this._value--; },
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Геттер, возвращающий текущее значение счётчика.
|
|
1157
|
+
* @type {number}
|
|
1158
|
+
*/
|
|
1159
|
+
get value() { return this._value; }
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* @file src/core.js
|
|
1164
|
+
*
|
|
1165
|
+
* @module sj-superellipse/core
|
|
1166
|
+
* @since 1.0.0
|
|
1167
|
+
* @author f4n70m
|
|
1168
|
+
*
|
|
1169
|
+
* @description
|
|
1170
|
+
* Ядро математических расчётов суперэллипса. Содержит функцию `jsse_generateSuperellipsePath`,
|
|
1171
|
+
* которая по заданным ширине, высоте, радиусу скругления и коэффициенту кривизны генерирует
|
|
1172
|
+
* SVG-путь, аппроксимирующий суперэллипс с помощью кубических кривых Безье.
|
|
1173
|
+
*
|
|
1174
|
+
* Также экспортирует вспомогательные функции:
|
|
1175
|
+
* - `jsse_roundf` – округление чисел с заданной точностью,
|
|
1176
|
+
* - `jsse_getBorderRadiusFactor` – возвращает константу (4/3)*(√2-1) для аппроксимации окружности.
|
|
1177
|
+
*
|
|
1178
|
+
* @see {@link https://en.wikipedia.org/wiki/Superellipse|Суперэллипс}
|
|
1179
|
+
* @see {@link https://github.com/f4n70m/js-superellipse|f4n70m/sj-superellipse}
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* import { jsse_generateSuperellipsePath } from 'js-superellipse/core';
|
|
1183
|
+
* const path = jsse_generateSuperellipsePath(200, 150, 30, 1.2);
|
|
1184
|
+
* svgPathElement.setAttribute('d', path);
|
|
1185
|
+
*/
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Округляет число до заданного количества знаков после запятой.
|
|
1189
|
+
*
|
|
1190
|
+
* @since 1.0.0
|
|
1191
|
+
* @param {number} value - Исходное число.
|
|
1192
|
+
* @param {number} [precision=2] - Количество знаков после запятой (по умолчанию 2).
|
|
1193
|
+
* @returns {number} Округлённое число.
|
|
1194
|
+
*/
|
|
1195
|
+
function jsse_roundf(value, precision = 2) {
|
|
1196
|
+
const factor = 10 ** precision;
|
|
1197
|
+
return (Math.round(value * factor) / factor).toFixed(precision);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Вычисляет параметр D2 для второй кривой Безье, обеспечивая касание
|
|
1202
|
+
* центра первой кривой с центром второй.
|
|
1203
|
+
*
|
|
1204
|
+
* В контексте суперэллипса параметры L (размах) и D (отклонение контрольных точек)
|
|
1205
|
+
* описывают форму кубической кривой Безье, используемой для построения углов.
|
|
1206
|
+
* Зная эталонную пару (L1, D1) и желаемый размах L2, функция находит такое D2,
|
|
1207
|
+
* при котором центральная точка второй кривой будет лежать на касательной,
|
|
1208
|
+
* проведённой из центра первой кривой (обеспечивается гладкое соединение).
|
|
1209
|
+
*
|
|
1210
|
+
* @since 1.0.0
|
|
1211
|
+
* @param {number} L1 - Размах эталонной кривой Безье.
|
|
1212
|
+
* @param {number} D1 - Отклонение контрольных точек эталонной кривой.
|
|
1213
|
+
* @param {number} L2 - Размах целевой кривой Безье.
|
|
1214
|
+
* @returns {number} Вычисленное значение D2, ограниченное сверху 1.
|
|
1215
|
+
* @throws {Error} Если L2 близко к нулю.
|
|
1216
|
+
*/
|
|
1217
|
+
function jsse_getD2FromL1D1L2(L1, D1, L2) {
|
|
1218
|
+
if (Math.abs(L2) < 1e-10) {
|
|
1219
|
+
throw new Error('L2 не может быть нулевым');
|
|
1220
|
+
}
|
|
1221
|
+
const D2 = (4 * L2 - L1 * (4 - 3 * D1)) / (3 * L2);
|
|
1222
|
+
return Math.min(D2, 1);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* @since 1.0.0
|
|
1227
|
+
*/
|
|
1228
|
+
function jsse_getBorderRadiusFactor() {
|
|
1229
|
+
return (4 / 3) * (Math.sqrt(2) - 1);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
*
|
|
1234
|
+
* Основная функция генерации SVG path для суперэллипса с изменяемой формой углов.
|
|
1235
|
+
* Форма углов определяется параметром curveFactor (от -2 до 2):
|
|
1236
|
+
* -2 : вогнутые прямоугольные углы
|
|
1237
|
+
* -G : вогнутые круглые углы (G = (4/3)*(√2-1) ≈ 0.5523)
|
|
1238
|
+
* 0 : прямой скос
|
|
1239
|
+
* G : выпуклые круглые углы
|
|
1240
|
+
* 1 : выгнутые суперэллипсные углы
|
|
1241
|
+
* 2 : выгнутые прямоугольные углы
|
|
1242
|
+
*
|
|
1243
|
+
* Функция генерирует непрерывный спектр форм от вогнутых прямоугольных (-2) до выгнутых прямоугольных (2), проходя через скос (0), круглые углы (±G) и суперэллипсные (1);
|
|
1244
|
+
* где G = (4/3)*(√2-1) ≈ 0.5522847498 — константа, при которой кривые Безье аппроксимируют четверть окружности.
|
|
1245
|
+
* Благодаря использованию кубических кривых Безье и тщательно подобранной интерполяции достигается плавное изменение геометрии при любом curveFactor в заданном диапазоне.
|
|
1246
|
+
*
|
|
1247
|
+
* @since 1.0.0
|
|
1248
|
+
* @param {number} width - Ширина фигуры.
|
|
1249
|
+
* @param {number} height - Высота фигуры.
|
|
1250
|
+
* @param {number} radius - Радиус скругления углов (будет автоматически ограничен).
|
|
1251
|
+
* @param {number} curveFactor - Коэффициент формы углов, диапазон [-2, 2].
|
|
1252
|
+
* @param {number} [precision=2] - Количество знаков после запятой в координатах.
|
|
1253
|
+
* @returns {string} Строка с SVG-командами для элемента <path>.
|
|
1254
|
+
* @throws {Error} Если при вычислениях возникает деление на ноль (маловероятно при корректных параметрах).
|
|
1255
|
+
*
|
|
1256
|
+
*/
|
|
1257
|
+
function jsse_generateSuperellipsePath(width, height, radius, curveFactor, precision = 2) {
|
|
1258
|
+
if (width <= 0 || height <= 0) {
|
|
1259
|
+
return "M0,0"; // или "M0,0" – пустой путь
|
|
1260
|
+
}
|
|
1261
|
+
if (typeof radius !== 'number' || isNaN(radius)) {
|
|
1262
|
+
radius = 0;
|
|
1263
|
+
}
|
|
1264
|
+
/** константа идеальной окружности **/
|
|
1265
|
+
const G = jsse_getBorderRadiusFactor();
|
|
1266
|
+
/** константа максимальной L при D == 1 **/
|
|
1267
|
+
const J = 8 - 4 * Math.sqrt(2);
|
|
1268
|
+
|
|
1269
|
+
/** Множитель для нормализации координат **/
|
|
1270
|
+
let M = width >= height ? width : height;
|
|
1271
|
+
|
|
1272
|
+
let kValue = Math.abs(curveFactor);
|
|
1273
|
+
let kSign = curveFactor >= 0 ? 1 : -1;
|
|
1274
|
+
|
|
1275
|
+
/** Ограничиваем радиус половиной меньшей стороны **/
|
|
1276
|
+
let rxMax = width / 2;
|
|
1277
|
+
let ryMax = height / 2;
|
|
1278
|
+
let rMax = Math.min(rxMax, ryMax);
|
|
1279
|
+
let r = Math.min(radius, rMax);
|
|
1280
|
+
let rMaxSpan = Math.abs(rxMax - ryMax);
|
|
1281
|
+
if (rxMax >= ryMax) {
|
|
1282
|
+
rxMax = ryMax + Math.min(rMax / 4, rMaxSpan / 4);
|
|
1283
|
+
} else {
|
|
1284
|
+
ryMax = rxMax + Math.min(rMax / 4, rMaxSpan / 4);
|
|
1285
|
+
}
|
|
1286
|
+
let rx = Math.min(r, rxMax);
|
|
1287
|
+
let ry = Math.min(r, ryMax);
|
|
1288
|
+
|
|
1289
|
+
let Dx = 0, Dy = 0;
|
|
1290
|
+
let Lx = 0, Ly = 0;
|
|
1291
|
+
let Sx = 0, Sy = 0;
|
|
1292
|
+
|
|
1293
|
+
if (r !== 0) {
|
|
1294
|
+
let R = 1;
|
|
1295
|
+
|
|
1296
|
+
/** Определить эллипсные (k=1) **/
|
|
1297
|
+
let Rk1x = rxMax / rx;
|
|
1298
|
+
let Rk1y = ryMax / ry;
|
|
1299
|
+
let Lk1x = (kSign > 0) ? Math.min(Rk1x, J) : 1;
|
|
1300
|
+
let Lk1y = (kSign > 0) ? Math.min(Rk1y, J) : 1;
|
|
1301
|
+
jsse_getD2FromL1D1L2(R, G, Lk1x);
|
|
1302
|
+
jsse_getD2FromL1D1L2(R, G, Lk1y);
|
|
1303
|
+
let Jk1x = (1 / J) * Lk1x;
|
|
1304
|
+
let Jk1y = (1 / J) * Lk1y;
|
|
1305
|
+
|
|
1306
|
+
/** Относительное L (от 1 до Lk1, при k от G до 1) **/
|
|
1307
|
+
let Lk = Math.max((Math.min(kValue, 1) - G) / (1 - G), 0);
|
|
1308
|
+
let Lix = 1 + (Lk1x - 1) * Lk;
|
|
1309
|
+
let Liy = 1 + (Lk1y - 1) * Lk;
|
|
1310
|
+
|
|
1311
|
+
/** Определить Di (от Li) **/
|
|
1312
|
+
let Six = 0, Siy = 0;
|
|
1313
|
+
let Dix, Diy;
|
|
1314
|
+
if (kValue <= G) {
|
|
1315
|
+
Dix = Diy = kValue;
|
|
1316
|
+
} else {
|
|
1317
|
+
let Dlix = jsse_getD2FromL1D1L2(R, G, Lix);
|
|
1318
|
+
let Dliy = jsse_getD2FromL1D1L2(R, G, Liy);
|
|
1319
|
+
if (kValue <= 1) {
|
|
1320
|
+
Dix = Dlix;
|
|
1321
|
+
Diy = Dliy;
|
|
1322
|
+
} else {
|
|
1323
|
+
Jk1x = G + (1 / (Lk1x / J)) * (1 - G);
|
|
1324
|
+
Jk1y = G + (1 / (Lk1y / J)) * (1 - G);
|
|
1325
|
+
let Jix = Math.min((kValue - 1) / (Jk1x - 1), 1);
|
|
1326
|
+
let Jiy = Math.min((kValue - 1) / (Jk1y - 1), 1);
|
|
1327
|
+
|
|
1328
|
+
let Lsx = Lix + (J - Lix) * Jix;
|
|
1329
|
+
let Lsy = Liy + (J - Liy) * Jiy;
|
|
1330
|
+
let Dlsx = jsse_getD2FromL1D1L2(R, G, Lsx);
|
|
1331
|
+
let Dlsy = jsse_getD2FromL1D1L2(R, G, Lsy);
|
|
1332
|
+
Dix = Math.min(Dlsx, 1);
|
|
1333
|
+
Diy = Math.min(Dlsy, 1);
|
|
1334
|
+
|
|
1335
|
+
if (kValue > Jk1x) {
|
|
1336
|
+
Six = (kValue - Jk1x) / (2 - Jk1x);
|
|
1337
|
+
}
|
|
1338
|
+
if (kValue > Jk1y) {
|
|
1339
|
+
Siy = (kValue - Jk1y) / (2 - Jk1y);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
Lx = Lix * (1 - Six);
|
|
1345
|
+
Ly = Liy * (1 - Siy);
|
|
1346
|
+
Sx = Lix * Six;
|
|
1347
|
+
Sy = Liy * Siy;
|
|
1348
|
+
Dx = Lx * Dix;
|
|
1349
|
+
Dy = Ly * Diy;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
let Qm = M;
|
|
1353
|
+
let Qw = width / M;
|
|
1354
|
+
let Qh = height / M;
|
|
1355
|
+
let Qr = r / M;
|
|
1356
|
+
let Q0 = { x: 0, y: 0 };
|
|
1357
|
+
let Q1 = { x: Qw, y: 0 };
|
|
1358
|
+
let Q2 = { x: Qw, y: Qh };
|
|
1359
|
+
let Q3 = { x: 0, y: Qh };
|
|
1360
|
+
|
|
1361
|
+
let pathCommands;
|
|
1362
|
+
if (kSign >= 0) {
|
|
1363
|
+
pathCommands = [
|
|
1364
|
+
`M`, Q0.x, Q0.y + (Sy + Ly) * Qr,
|
|
1365
|
+
`L`, Q0.x, Q0.y + (Ly) * Qr,
|
|
1366
|
+
`C`, Q0.x, Q0.y + (Ly - Dy) * Qr, Q0.x + (Lx - Dx) * Qr, Q0.y, Q0.x + (Lx) * Qr, Q0.y,
|
|
1367
|
+
`L`, Q0.x + (Sx + Lx) * Qr, Q0.y,
|
|
1368
|
+
`L`, Q1.x - (Sx + Lx) * Qr, Q1.y,
|
|
1369
|
+
`L`, Q1.x - (Lx) * Qr, Q1.y,
|
|
1370
|
+
`C`, Q1.x - (Lx - Dx) * Qr, Q1.y, Q1.x, Q1.y + (Ly - Dy) * Qr, Q1.x, Q1.y + (Ly) * Qr,
|
|
1371
|
+
`L`, Q1.x, Q1.y + (Sy + Ly) * Qr,
|
|
1372
|
+
`L`, Q2.x, Q2.y - (Sy + Ly) * Qr,
|
|
1373
|
+
`L`, Q2.x, Q2.y - (Ly) * Qr,
|
|
1374
|
+
`C`, Q2.x, Q2.y - (Ly - Dy) * Qr, Q2.x - (Lx - Dx) * Qr, Q2.y, Q2.x - (Lx) * Qr, Q2.y,
|
|
1375
|
+
`L`, Q2.x - (Sx + Lx) * Qr, Q2.y,
|
|
1376
|
+
`L`, Q3.x + (Sx + Lx) * Qr, Q3.y,
|
|
1377
|
+
`L`, Q3.x + (Lx) * Qr, Q3.y,
|
|
1378
|
+
`C`, Q3.x + (Lx - Dx) * Qr, Q3.y, Q3.x, Q3.y - (Ly - Dy) * Qr, Q3.x, Q3.y - (Ly) * Qr,
|
|
1379
|
+
`L`, Q3.x, Q3.y - (Sy + Ly) * Qr,
|
|
1380
|
+
'Z'
|
|
1381
|
+
];
|
|
1382
|
+
} else {
|
|
1383
|
+
pathCommands = [
|
|
1384
|
+
`M`, Q0.x, Q0.y + (Sy + Ly) * Qr,
|
|
1385
|
+
`L`, Q0.x + (Sx) * Qr, Q0.y + (Sy + Ly) * Qr,
|
|
1386
|
+
`C`, Q0.x + (Sx + Dx) * Qr, Q0.y + (Sy + Ly) * Qr, Q0.x + (Sx + Lx) * Qr, Q0.y + (Sy + Dy) * Qr, Q0.x + (Sx + Lx) * Qr, Q0.y + (Sy) * Qr,
|
|
1387
|
+
`L`, Q0.x + (Sx + Lx) * Qr, Q0.y,
|
|
1388
|
+
`L`, Q1.x - (Sx + Lx) * Qr, Q1.y,
|
|
1389
|
+
`L`, Q1.x - (Sx + Lx) * Qr, Q1.y + (Sy) * Qr,
|
|
1390
|
+
`C`, Q1.x - (Sx + Lx) * Qr, Q1.y + (Sy + Dy) * Qr, Q1.x - (Sx + Dx) * Qr, Q1.y + (Sy + Ly) * Qr, Q1.x - (Sx) * Qr, Q1.y + (Sy + Ly) * Qr,
|
|
1391
|
+
`L`, Q1.x, Q1.y + (Sy + Ly) * Qr,
|
|
1392
|
+
`L`, Q2.x, Q2.y - (Sy + Ly) * Qr,
|
|
1393
|
+
`L`, Q2.x - Sx * Qr, Q2.y - (Sy + Ly) * Qr,
|
|
1394
|
+
`C`, Q2.x - (Sx + Dx) * Qr, Q2.y - (Sy + Ly) * Qr, Q2.x - (Sx + Lx) * Qr, Q2.y - (Sy + Dy) * Qr, Q2.x - (Sx + Lx) * Qr, Q2.y - (Sy) * Qr,
|
|
1395
|
+
`L`, Q2.x - (Sx + Lx) * Qr, Q2.y,
|
|
1396
|
+
`L`, Q3.x + (Sx + Lx) * Qr, Q3.y,
|
|
1397
|
+
`L`, Q3.x + (Sx + Lx) * Qr, Q3.y - Sy * Qr,
|
|
1398
|
+
`C`, Q3.x + (Sx + Lx) * Qr, Q3.y - (Sy + Dy) * Qr, Q3.x + (Sx + Dx) * Qr, Q3.y - (Sy + Ly) * Qr, Q3.x + Sx * Qr, Q3.y - (Sy + Ly) * Qr,
|
|
1399
|
+
`L`, Q3.x, Q3.y - (Sy + Ly) * Qr,
|
|
1400
|
+
'Z'
|
|
1401
|
+
];
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** Применяем масштабирование и округление **/
|
|
1405
|
+
const path = pathCommands.map(p => {
|
|
1406
|
+
if (typeof p === 'number') {
|
|
1407
|
+
return jsse_roundf(p * Qm, precision);
|
|
1408
|
+
}
|
|
1409
|
+
return p;
|
|
1410
|
+
}).join(' ');
|
|
1411
|
+
|
|
1412
|
+
return path;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* @file src/mode.js
|
|
1417
|
+
*
|
|
1418
|
+
* @module sj-superellipse/mode
|
|
1419
|
+
* @since 1.0.0
|
|
1420
|
+
* @author f4n70m
|
|
1421
|
+
*
|
|
1422
|
+
* @description
|
|
1423
|
+
* Базовый класс `SuperellipseMode`, от которого наследуются конкретные реализации режимов.
|
|
1424
|
+
* Содержит общую логику захвата стилей и размеров элемента, пересчёта SVG-пути суперэллипса,
|
|
1425
|
+
* применения/восстановления CSS-свойств (`clip-path`). Определяет защищённые методы, которые должны
|
|
1426
|
+
* быть переопределены в дочерних классах.
|
|
1427
|
+
*
|
|
1428
|
+
* @example
|
|
1429
|
+
* class MyMode extends SuperellipseMode {
|
|
1430
|
+
* _getActivatedStyles() { return { /* стили активации *\}; }
|
|
1431
|
+
* _getModeName() { return 'my-mode'; }
|
|
1432
|
+
* _appendVirtualElements() { /* реализация *\/ }
|
|
1433
|
+
* _removeVirtualElements() { /* реализация *\/ }
|
|
1434
|
+
* }
|
|
1435
|
+
*/
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Базовый класс для реализации режимов суперэллипса.
|
|
1440
|
+
* @class SuperellipseMode
|
|
1441
|
+
* @since 1.0.0
|
|
1442
|
+
*/
|
|
1443
|
+
class SuperellipseMode {
|
|
1444
|
+
|
|
1445
|
+
_element;
|
|
1446
|
+
|
|
1447
|
+
_isInitiated;
|
|
1448
|
+
_isActivated;
|
|
1449
|
+
// _isDebug;
|
|
1450
|
+
|
|
1451
|
+
_size = {
|
|
1452
|
+
width: 0,
|
|
1453
|
+
height: 0
|
|
1454
|
+
};
|
|
1455
|
+
_curveFactor;
|
|
1456
|
+
_precision;
|
|
1457
|
+
|
|
1458
|
+
_styles;
|
|
1459
|
+
|
|
1460
|
+
_path;
|
|
1461
|
+
_resetPath;
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* =============================================================
|
|
1468
|
+
* PUBLIC
|
|
1469
|
+
* =============================================================
|
|
1470
|
+
*/
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* @since 1.0.0
|
|
1475
|
+
* @param {Element} element - Целевой элемент.
|
|
1476
|
+
* @param {boolean} [debug=false] - Флаг отладки.
|
|
1477
|
+
*/
|
|
1478
|
+
constructor(element) {
|
|
1479
|
+
|
|
1480
|
+
this._element = element;
|
|
1481
|
+
|
|
1482
|
+
// this._isDebug = debug;
|
|
1483
|
+
this._isActivated = false;
|
|
1484
|
+
|
|
1485
|
+
this._curveFactor = jsse_getBorderRadiusFactor();
|
|
1486
|
+
this._precision = 2;
|
|
1487
|
+
|
|
1488
|
+
this._init();
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* Активирует режим.
|
|
1493
|
+
* @since 1.0.0
|
|
1494
|
+
* @returns {void}
|
|
1495
|
+
*/
|
|
1496
|
+
activate() {
|
|
1497
|
+
if (this.isActivated()) return;
|
|
1498
|
+
jsse_console.debug({label:'MODE',element:this._element}, `activate(${this._getModeName()})`);
|
|
1499
|
+
/** Актуализировать данные захвата **/
|
|
1500
|
+
this._updateCaptured();
|
|
1501
|
+
/** Установить статус **/
|
|
1502
|
+
this._setStatus(true);
|
|
1503
|
+
/** Создать виртуальные элементы **/
|
|
1504
|
+
this._appendVirtualElements();
|
|
1505
|
+
/** Подготовить обновление **/
|
|
1506
|
+
this._prepareUpdate();
|
|
1507
|
+
/** Выполнить обновление **/
|
|
1508
|
+
this._executeUpdate();
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Деактивирует режим.
|
|
1513
|
+
* @since 1.0.0
|
|
1514
|
+
* @returns {void}
|
|
1515
|
+
*/
|
|
1516
|
+
deactivate() {
|
|
1517
|
+
if (!this.isActivated()) return;
|
|
1518
|
+
jsse_console.debug({label:'MODE',element:this._element}, `deactivate(${this._getModeName()})`);
|
|
1519
|
+
/** Установить статус **/
|
|
1520
|
+
this._setStatus(false);
|
|
1521
|
+
/** Удалить элементы виртуальных слоев **/
|
|
1522
|
+
this._removeVirtualElements();
|
|
1523
|
+
/** Выполнить обновление **/
|
|
1524
|
+
this._executeUpdate();
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Полное обновление (стили, размер, путь).
|
|
1529
|
+
* @since 1.0.0
|
|
1530
|
+
* @returns {void}
|
|
1531
|
+
*/
|
|
1532
|
+
update() {
|
|
1533
|
+
jsse_console.debug({label:'MODE',element:this._element}, 'update()');
|
|
1534
|
+
/** Актуализировать данные захвата **/
|
|
1535
|
+
this._updateCaptured();
|
|
1536
|
+
/** Подготовить обновление **/
|
|
1537
|
+
this._prepareUpdate();
|
|
1538
|
+
/** Выполнить обновление **/
|
|
1539
|
+
this._executeUpdate();
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Обновление только размеров.
|
|
1544
|
+
* @since 1.0.0
|
|
1545
|
+
* @returns {void}
|
|
1546
|
+
*/
|
|
1547
|
+
updateSize() {
|
|
1548
|
+
/** Актуализировать размеры **/
|
|
1549
|
+
this._updateCapturedSize();
|
|
1550
|
+
/** Подготовить обновление **/
|
|
1551
|
+
this._prepareUpdate();
|
|
1552
|
+
/** Выполнить обновление **/
|
|
1553
|
+
this._executeUpdate();
|
|
1554
|
+
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Обновление только стилей.
|
|
1559
|
+
* @since 1.0.0
|
|
1560
|
+
* @returns {void}
|
|
1561
|
+
*/
|
|
1562
|
+
updateStyles() {
|
|
1563
|
+
jsse_console.debug({label:'MODE',element:this._element}, 'updateStyles()');
|
|
1564
|
+
/** Актуализировать стили **/
|
|
1565
|
+
this._updateCapturedStyles();
|
|
1566
|
+
/** Подготовить обновление **/
|
|
1567
|
+
this._prepareUpdate();
|
|
1568
|
+
/** Выполнить обновление **/
|
|
1569
|
+
this._executeUpdate();
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* Обновление коэффициента кривизны.
|
|
1574
|
+
* @since 1.0.0
|
|
1575
|
+
* @param {number} value - Новое значение коэффициента кривизны.
|
|
1576
|
+
* @returns {void}
|
|
1577
|
+
*/
|
|
1578
|
+
updateCurveFactor(value) {
|
|
1579
|
+
this.setCurveFactor(value);
|
|
1580
|
+
/** Подготовить обновление **/
|
|
1581
|
+
this._prepareUpdate();
|
|
1582
|
+
/** Выполнить обновление **/
|
|
1583
|
+
this._executeUpdate();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* Обновление точности округления.
|
|
1588
|
+
* @since 1.0.0
|
|
1589
|
+
* @param {number} value - Количество знаков после запятой.
|
|
1590
|
+
* @returns {void}
|
|
1591
|
+
*/
|
|
1592
|
+
updatePrecision(value) {
|
|
1593
|
+
this.setPrecision(value);
|
|
1594
|
+
/** Подготовить обновление **/
|
|
1595
|
+
this._prepareUpdate();
|
|
1596
|
+
/** Выполнить обновление **/
|
|
1597
|
+
this._executeUpdate();
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* Устанавливает коэффициент кривизны.
|
|
1602
|
+
* @since 1.0.0
|
|
1603
|
+
* @param {number} value - Новое значение коэффициента кривизны.
|
|
1604
|
+
* @returns {void}
|
|
1605
|
+
*/
|
|
1606
|
+
setCurveFactor(value) {
|
|
1607
|
+
this._curveFactor = value;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Устанавливает точность округления.
|
|
1612
|
+
* @since 1.0.0
|
|
1613
|
+
* @param {number} value - Количество знаков после запятой.
|
|
1614
|
+
* @returns {void}
|
|
1615
|
+
*/
|
|
1616
|
+
setPrecision(value) {
|
|
1617
|
+
this._precision = value;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Возвращает текущий SVG-путь.
|
|
1622
|
+
* @since 1.0.0
|
|
1623
|
+
* @returns {string}
|
|
1624
|
+
*/
|
|
1625
|
+
getPath() {
|
|
1626
|
+
return this._path;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* Проверяет, активирован ли режим.
|
|
1631
|
+
* @since 1.0.0
|
|
1632
|
+
* @returns {boolean}
|
|
1633
|
+
*/
|
|
1634
|
+
isActivated() {
|
|
1635
|
+
return this._isActivated;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Уничтожает режим, удаляет все артефакты.
|
|
1640
|
+
* @since 1.0.0
|
|
1641
|
+
* @returns {void}
|
|
1642
|
+
*/
|
|
1643
|
+
destroy() {
|
|
1644
|
+
this.deactivate();
|
|
1645
|
+
this._removeModeAttr();
|
|
1646
|
+
this._destroyResetStyles();
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* =============================================================
|
|
1652
|
+
* PRIVATE
|
|
1653
|
+
* =============================================================
|
|
1654
|
+
*/
|
|
1655
|
+
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Инициализация режима.
|
|
1659
|
+
* @since 1.0.0
|
|
1660
|
+
* @private
|
|
1661
|
+
* @returns {void}
|
|
1662
|
+
*/
|
|
1663
|
+
_init() {
|
|
1664
|
+
this._id = Math.random().toString(36).slice(2, 10);
|
|
1665
|
+
this._initStyles();
|
|
1666
|
+
this._setModeAttr();
|
|
1667
|
+
this._initSize();
|
|
1668
|
+
this._initCurve();
|
|
1669
|
+
this._isInitiated = true;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* Устанавливает статус активации.
|
|
1674
|
+
* @since 1.0.0
|
|
1675
|
+
* @private
|
|
1676
|
+
* @param {boolean} status - Статус активации.
|
|
1677
|
+
* @returns {void}
|
|
1678
|
+
*/
|
|
1679
|
+
_setStatus(status) {
|
|
1680
|
+
this._isActivated = status;
|
|
1681
|
+
if (status) {
|
|
1682
|
+
this._setActivatedAttr();
|
|
1683
|
+
} else {
|
|
1684
|
+
this._removeActivatedAttr();
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Захватывает актуальные стили и размеры.
|
|
1690
|
+
* @since 1.0.0
|
|
1691
|
+
* @private
|
|
1692
|
+
* @returns {void}
|
|
1693
|
+
*/
|
|
1694
|
+
_updateCaptured() {
|
|
1695
|
+
this._updateCapturedStyles();
|
|
1696
|
+
this._updateCapturedSize();
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Подготавливает обновление (пересчёт кривой).
|
|
1701
|
+
* @since 1.0.0
|
|
1702
|
+
* @private
|
|
1703
|
+
* @returns {void}
|
|
1704
|
+
*/
|
|
1705
|
+
_prepareUpdate() {
|
|
1706
|
+
this._recalculateCurve();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* Выполняет обновление (применяет кривую).
|
|
1711
|
+
* @since 1.0.0
|
|
1712
|
+
* @private
|
|
1713
|
+
* @returns {void}
|
|
1714
|
+
*/
|
|
1715
|
+
_executeUpdate() {
|
|
1716
|
+
this._applyCurrentCurve();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Возвращает имя режима.
|
|
1721
|
+
* @since 1.0.0
|
|
1722
|
+
* @protected
|
|
1723
|
+
* @returns {string}
|
|
1724
|
+
*/
|
|
1725
|
+
_getModeName() {
|
|
1726
|
+
return 'clip-path';
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Возвращает карту стилей, которые нужно временно применить для корректного чтения.
|
|
1731
|
+
* @since 1.0.0
|
|
1732
|
+
* @protected
|
|
1733
|
+
* @returns {Object<string, string>}
|
|
1734
|
+
*/
|
|
1735
|
+
_getReadingStyles() {
|
|
1736
|
+
return {
|
|
1737
|
+
'transition': 'unset'
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Возвращает карту стилей, применяемых при активации режима.
|
|
1743
|
+
* @since 1.0.0
|
|
1744
|
+
* @protected
|
|
1745
|
+
* @returns {Object<string, string>}
|
|
1746
|
+
*/
|
|
1747
|
+
_getActivatedStyles() {
|
|
1748
|
+
return {
|
|
1749
|
+
'border-radius': '0px'
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* =============================================================
|
|
1756
|
+
* VIRTUAL
|
|
1757
|
+
* =============================================================
|
|
1758
|
+
*/
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Создаёт виртуальные элементы (если нужно).
|
|
1763
|
+
* @since 1.0.0
|
|
1764
|
+
* @protected
|
|
1765
|
+
* @returns {void}
|
|
1766
|
+
*/
|
|
1767
|
+
_appendVirtualElements() {}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Удаляет виртуальные элементы.
|
|
1771
|
+
* @since 1.0.0
|
|
1772
|
+
* @protected
|
|
1773
|
+
* @returns {void}
|
|
1774
|
+
*/
|
|
1775
|
+
_removeVirtualElements() {}
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
/**
|
|
1779
|
+
* =============================================================
|
|
1780
|
+
* ATTRIBUTES
|
|
1781
|
+
* =============================================================
|
|
1782
|
+
*/
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Устанавливает атрибут `data-jsse-mode`.
|
|
1787
|
+
* @since 1.0.0
|
|
1788
|
+
* @protected
|
|
1789
|
+
* @returns {void}
|
|
1790
|
+
*/
|
|
1791
|
+
_setModeAttr() {
|
|
1792
|
+
this._element.setAttribute('data-jsse-mode', this._getModeName());
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
/**
|
|
1796
|
+
* Удаляет атрибут `data-jsse-mode`.
|
|
1797
|
+
* @since 1.0.0
|
|
1798
|
+
* @protected
|
|
1799
|
+
* @returns {void}
|
|
1800
|
+
*/
|
|
1801
|
+
_removeModeAttr() {
|
|
1802
|
+
this._element.removeAttribute('data-jsse-mode');
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/**
|
|
1806
|
+
* Устанавливает атрибут `data-jsse-activated`.
|
|
1807
|
+
* @since 1.0.0
|
|
1808
|
+
* @protected
|
|
1809
|
+
* @returns {void}
|
|
1810
|
+
*/
|
|
1811
|
+
_setActivatedAttr() {
|
|
1812
|
+
this._element.setAttribute('data-jsse-activated', true);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/**
|
|
1816
|
+
* Удаляет атрибут `data-jsse-activated`.
|
|
1817
|
+
* @since 1.0.0
|
|
1818
|
+
* @protected
|
|
1819
|
+
* @returns {void}
|
|
1820
|
+
*/
|
|
1821
|
+
_removeActivatedAttr() {
|
|
1822
|
+
this._element.removeAttribute('data-jsse-activated');
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Устанавливает атрибут `data-jsse-reading`.
|
|
1827
|
+
* @since 1.0.0
|
|
1828
|
+
* @protected
|
|
1829
|
+
* @returns {void}
|
|
1830
|
+
*/
|
|
1831
|
+
_setReadingAttr() {
|
|
1832
|
+
this._element.setAttribute('data-jsse-reading', true);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Удаляет атрибут `data-jsse-reading`.
|
|
1837
|
+
* @since 1.0.0
|
|
1838
|
+
* @protected
|
|
1839
|
+
* @returns {void}
|
|
1840
|
+
*/
|
|
1841
|
+
_removeReadingAttr() {
|
|
1842
|
+
this._element.removeAttribute('data-jsse-reading');
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* =============================================================
|
|
1848
|
+
* CSS
|
|
1849
|
+
* =============================================================
|
|
1850
|
+
*/
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Инициализирует глобальные CSS-правила для режима.
|
|
1855
|
+
* @since 1.0.0
|
|
1856
|
+
* @protected
|
|
1857
|
+
* @returns {void}
|
|
1858
|
+
*/
|
|
1859
|
+
_initResetStyles() {
|
|
1860
|
+
const modeName = this._getModeName();
|
|
1861
|
+
if (!jsse_reset_css.has(modeName)) {
|
|
1862
|
+
const styleElement = this._createModeCssStyleElement(modeName);
|
|
1863
|
+
jsse_reset_css.set(modeName, styleElement);
|
|
1864
|
+
} else {
|
|
1865
|
+
jsse_reset_css.get(modeName).count++;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
jsse_console.debug({label:'MODE',element:this._element}, '[RESET STYLES]', 'INIT');
|
|
1869
|
+
// jsse_console.debug({label:'MODE'}, '[RESET STYLES]', '[INIT]', modeName, jsse_reset_css.get(modeName).count);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Уничтожает глобальные CSS-правила режима.
|
|
1874
|
+
* @since 1.0.0
|
|
1875
|
+
* @protected
|
|
1876
|
+
* @returns {void}
|
|
1877
|
+
*/
|
|
1878
|
+
_destroyResetStyles() {
|
|
1879
|
+
const modeName = this._getModeName();
|
|
1880
|
+
if (!jsse_reset_css.has(modeName)) return;
|
|
1881
|
+
|
|
1882
|
+
const modeResetStyle = jsse_reset_css.get(modeName);
|
|
1883
|
+
modeResetStyle.count--;
|
|
1884
|
+
|
|
1885
|
+
jsse_console.debug({label:'MODE',element:this._element}, '[RESET STYLES]', '[DESTROY]');
|
|
1886
|
+
// jsse_console.debug({label:'MODE'}, '[RESET STYLES]', '[DESTROY]', modeName, jsse_reset_css.get(modeName).count);
|
|
1887
|
+
|
|
1888
|
+
if (modeResetStyle.count <= 0) {
|
|
1889
|
+
jsse_reset_css.unset(modeName);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/**
|
|
1895
|
+
* Возвращает CSS-текст для сброса стилей режима.
|
|
1896
|
+
* @since 1.0.0
|
|
1897
|
+
* @protected
|
|
1898
|
+
* @param {string} modeName - Имя режима.
|
|
1899
|
+
* @returns {string}
|
|
1900
|
+
*/
|
|
1901
|
+
_getResetCssText(modeName) {
|
|
1902
|
+
let cssString = '';
|
|
1903
|
+
|
|
1904
|
+
const activatedStyles = this._getActivatedStyles();
|
|
1905
|
+
cssString += `*:hover [data-jsse-mode="${modeName}"][data-jsse-activated=true],`;
|
|
1906
|
+
cssString += `[data-jsse-mode="${modeName}"][data-jsse-activated=true]:hover,`;
|
|
1907
|
+
cssString += `[data-jsse-mode="${modeName}"][data-jsse-activated=true]`;
|
|
1908
|
+
cssString += `{`;
|
|
1909
|
+
for (const prop in activatedStyles) {
|
|
1910
|
+
if (activatedStyles[prop] === '') continue;
|
|
1911
|
+
cssString += `\n\t${prop}: ${activatedStyles[prop]} !important;`;
|
|
1912
|
+
}
|
|
1913
|
+
cssString += `\n}`;
|
|
1914
|
+
|
|
1915
|
+
cssString += `\n`;
|
|
1916
|
+
|
|
1917
|
+
const readingStyles = this._getReadingStyles();
|
|
1918
|
+
cssString += `*:hover [data-jsse-mode="${modeName}"][data-jsse-reading=true],`;
|
|
1919
|
+
cssString += `[data-jsse-mode="${modeName}"][data-jsse-reading=true]:hover,`;
|
|
1920
|
+
cssString += `[data-jsse-mode="${modeName}"][data-jsse-reading=true]`;
|
|
1921
|
+
cssString += `{`;
|
|
1922
|
+
for (const prop in readingStyles) {
|
|
1923
|
+
if (readingStyles[prop] === '') continue;
|
|
1924
|
+
cssString += `\n\t${prop}: ${readingStyles[prop]} !important;`;
|
|
1925
|
+
}
|
|
1926
|
+
cssString += `\n}`;
|
|
1927
|
+
|
|
1928
|
+
return cssString;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/**
|
|
1932
|
+
* Создаёт элемент `<style>` для режима.
|
|
1933
|
+
* @since 1.0.0
|
|
1934
|
+
* @protected
|
|
1935
|
+
* @param {string} modeName - Имя режима.
|
|
1936
|
+
* @returns {HTMLStyleElement}
|
|
1937
|
+
*/
|
|
1938
|
+
_createModeCssStyleElement(modeName) {
|
|
1939
|
+
const textContent = this._getResetCssText(modeName);
|
|
1940
|
+
/** Создать элемент <style> **/
|
|
1941
|
+
const styleElement = document.createElement('style');
|
|
1942
|
+
styleElement.setAttribute('id', `jsse__css_${modeName}`);
|
|
1943
|
+
/** Заполнить элемент CSS-правилами **/
|
|
1944
|
+
styleElement.textContent = textContent;
|
|
1945
|
+
return styleElement;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* =============================================================
|
|
1951
|
+
* STYLES
|
|
1952
|
+
* =============================================================
|
|
1953
|
+
*/
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Обновляет захваченные стили.
|
|
1958
|
+
* @since 1.0.0
|
|
1959
|
+
* @protected
|
|
1960
|
+
* @returns {void}
|
|
1961
|
+
*/
|
|
1962
|
+
_updateCapturedStyles() {
|
|
1963
|
+
jsse_console.debug({label:'MODE',element:this._element}, '_updateCapturedStyles()');
|
|
1964
|
+
const capturedComputedStyles = this._getCapturedStyles();
|
|
1965
|
+
/** Сохранить computed-стили **/
|
|
1966
|
+
this._styles.computed = capturedComputedStyles;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Получает вычисленные стили с временным снятием атрибута активации.
|
|
1971
|
+
* @since 1.0.0
|
|
1972
|
+
* @protected
|
|
1973
|
+
* @param {boolean} [clear=true] - Снимать ли атрибут активации перед чтением.
|
|
1974
|
+
* @returns {Object<string, string>}
|
|
1975
|
+
*/
|
|
1976
|
+
_getCapturedStyles(clear = true) {
|
|
1977
|
+
const hasAttribute = this._element.hasAttribute('data-jsse-activated');
|
|
1978
|
+
|
|
1979
|
+
if (hasAttribute && clear) {
|
|
1980
|
+
this._removeActivatedAttr();
|
|
1981
|
+
}
|
|
1982
|
+
this._setReadingAttr();
|
|
1983
|
+
|
|
1984
|
+
const result = this._getManagedComputedStyle();
|
|
1985
|
+
|
|
1986
|
+
if (hasAttribute && clear) {
|
|
1987
|
+
this._setActivatedAttr();
|
|
1988
|
+
}
|
|
1989
|
+
this._removeReadingAttr();
|
|
1990
|
+
return result;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Получает вычисленные стили для управляемых свойств.
|
|
1995
|
+
* @since 1.0.0
|
|
1996
|
+
* @protected
|
|
1997
|
+
* @returns {Object<string, string>}
|
|
1998
|
+
*/
|
|
1999
|
+
_getManagedComputedStyle() {
|
|
2000
|
+
const result = {};
|
|
2001
|
+
const capturedStyles = getComputedStyle(this._element);
|
|
2002
|
+
for (const prop of this._getManagedProperties()) {
|
|
2003
|
+
result[prop] = capturedStyles.getPropertyValue(prop);
|
|
2004
|
+
}
|
|
2005
|
+
return result;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Возвращает значение захваченного вычисленного свойства.
|
|
2010
|
+
* @since 1.0.0
|
|
2011
|
+
* @protected
|
|
2012
|
+
* @param {string} prop - Имя CSS-свойства.
|
|
2013
|
+
* @returns {string|undefined}
|
|
2014
|
+
*/
|
|
2015
|
+
_getComputedProp(prop) {
|
|
2016
|
+
if ('computed' in this._styles && prop in this._styles.computed)
|
|
2017
|
+
return this._styles.computed[prop];
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Возвращает массив свойств CSS, управляемых режимом.
|
|
2022
|
+
* @since 1.0.0
|
|
2023
|
+
* @protected
|
|
2024
|
+
* @returns {string[]}
|
|
2025
|
+
*/
|
|
2026
|
+
_getManagedProperties() {
|
|
2027
|
+
return Object.keys(this._getActivatedStyles());
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* =============================================================
|
|
2033
|
+
* CACHE
|
|
2034
|
+
* =============================================================
|
|
2035
|
+
*/
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Инициализирует хранилище стилей.
|
|
2039
|
+
* @since 1.0.0
|
|
2040
|
+
* @protected
|
|
2041
|
+
* @returns {void}
|
|
2042
|
+
*/
|
|
2043
|
+
_initStyles() {
|
|
2044
|
+
this._styles = jsse_styles.get(this._element);
|
|
2045
|
+
this._initResetStyles();
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* =============================================================
|
|
2050
|
+
* SIZE
|
|
2051
|
+
* =============================================================
|
|
2052
|
+
*/
|
|
2053
|
+
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* Инициализирует размеры.
|
|
2057
|
+
* @since 1.0.0
|
|
2058
|
+
* @protected
|
|
2059
|
+
* @returns {void}
|
|
2060
|
+
*/
|
|
2061
|
+
_initSize() {
|
|
2062
|
+
this._updateCapturedSize();
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Обновляет захваченные размеры.
|
|
2067
|
+
* @since 1.0.0
|
|
2068
|
+
* @protected
|
|
2069
|
+
* @returns {void}
|
|
2070
|
+
*/
|
|
2071
|
+
_updateCapturedSize() {
|
|
2072
|
+
const rect = this._element.getBoundingClientRect();
|
|
2073
|
+
|
|
2074
|
+
this._size.width = rect.width;
|
|
2075
|
+
this._size.height = rect.height;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* =============================================================
|
|
2081
|
+
* CURVE
|
|
2082
|
+
* =============================================================
|
|
2083
|
+
*/
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* Инициализирует кривую.
|
|
2088
|
+
* @since 1.0.0
|
|
2089
|
+
* @protected
|
|
2090
|
+
* @returns {void}
|
|
2091
|
+
*/
|
|
2092
|
+
_initCurve() {
|
|
2093
|
+
this._initInlinePath();
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* Сохраняет исходный `clip-path`.
|
|
2098
|
+
* @since 1.0.0
|
|
2099
|
+
* @protected
|
|
2100
|
+
* @returns {void}
|
|
2101
|
+
*/
|
|
2102
|
+
_initInlinePath() {
|
|
2103
|
+
this._resetPath = this._element.style.getPropertyValue('clip-path');
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* Пересчитывает путь на основе текущих размеров и радиуса.
|
|
2108
|
+
* @since 1.0.0
|
|
2109
|
+
* @protected
|
|
2110
|
+
* @returns {void}
|
|
2111
|
+
*/
|
|
2112
|
+
_recalculateCurve() {
|
|
2113
|
+
this._recalculatePath();
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* Генерирует SVG-путь.
|
|
2118
|
+
* @since 1.0.0
|
|
2119
|
+
* @protected
|
|
2120
|
+
* @returns {void}
|
|
2121
|
+
*/
|
|
2122
|
+
_recalculatePath() {
|
|
2123
|
+
if ( !( this._size.width > 0 && this._size.height > 0 ) ) {
|
|
2124
|
+
this._path = 'none'; // Сбрасываем путь
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const radiusValue = this._getComputedProp('border-radius');
|
|
2129
|
+
const radiusNumber = radiusValue ? parseFloat(radiusValue) : 0;
|
|
2130
|
+
|
|
2131
|
+
this._path = jsse_generateSuperellipsePath(
|
|
2132
|
+
this._size.width,
|
|
2133
|
+
this._size.height,
|
|
2134
|
+
radiusNumber,
|
|
2135
|
+
this._curveFactor,
|
|
2136
|
+
this._precision
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Применяет текущий путь (если активирован) или восстанавливает исходный.
|
|
2142
|
+
* @since 1.0.0
|
|
2143
|
+
* @protected
|
|
2144
|
+
* @returns {void}
|
|
2145
|
+
*/
|
|
2146
|
+
_applyCurrentCurve() {
|
|
2147
|
+
if (this.isActivated()) {
|
|
2148
|
+
this._applyCurve();
|
|
2149
|
+
} else {
|
|
2150
|
+
this._restoreCurve();
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Применяет суперэллипс через `clip-path`.
|
|
2156
|
+
* @since 1.0.0
|
|
2157
|
+
* @protected
|
|
2158
|
+
* @returns {void}
|
|
2159
|
+
*/
|
|
2160
|
+
_applyCurve() {
|
|
2161
|
+
if (this._path && this._path !== 'none') {
|
|
2162
|
+
const newPath = `path("${this._path}")`;
|
|
2163
|
+
if (this._element.style.getPropertyValue('clip-path') !== newPath) {
|
|
2164
|
+
this._element.style.setProperty('clip-path', newPath);
|
|
2165
|
+
}
|
|
2166
|
+
} else {
|
|
2167
|
+
if (this._element.style.getPropertyValue('clip-path') !== 'none') {
|
|
2168
|
+
this._element.style.setProperty('clip-path', 'none');
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Восстанавливает исходный `clip-path`.
|
|
2175
|
+
* @since 1.0.0
|
|
2176
|
+
* @protected
|
|
2177
|
+
* @returns {void}
|
|
2178
|
+
*/
|
|
2179
|
+
_restoreCurve() {
|
|
2180
|
+
if (this._resetPath) {
|
|
2181
|
+
this._element.style.setProperty('clip-path', this._resetPath);
|
|
2182
|
+
} else {
|
|
2183
|
+
this._element.style.removeProperty('clip-path');
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* =============================================================
|
|
2190
|
+
*
|
|
2191
|
+
* =============================================================
|
|
2192
|
+
*/
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/**
|
|
2196
|
+
* @file src/mode-svg-layer.js
|
|
2197
|
+
*
|
|
2198
|
+
* @module sj-superellipse/mode-svg-layer
|
|
2199
|
+
* @since 1.0.0
|
|
2200
|
+
* @author f4n70m
|
|
2201
|
+
*
|
|
2202
|
+
* @description
|
|
2203
|
+
* Режим `svg-layer` – полнофункциональный режим, создающий наложенный SVG-слой для отрисовки
|
|
2204
|
+
* суперэллипса. Позволяет корректно отображать `background`, `border`, `box-shadow` элемента.
|
|
2205
|
+
* Переносит дочернее содержимое во внутренний div-контейнер, а поверх него размещает SVG,
|
|
2206
|
+
* который повторяет геометрию суперэллипса с возможностью применения градиентов, теней и
|
|
2207
|
+
* произвольных стилей обводки.
|
|
2208
|
+
*
|
|
2209
|
+
* @extends SuperellipseMode
|
|
2210
|
+
* @example
|
|
2211
|
+
* const mode = new SuperellipseModeSvgLayer(element);
|
|
2212
|
+
* mode.activate();
|
|
2213
|
+
*/
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
/**
|
|
2218
|
+
* Режим, создающий наложенный SVG-слой для отрисовки фона, границ и теней.
|
|
2219
|
+
* @class SuperellipseModeSvgLayer
|
|
2220
|
+
* @extends SuperellipseMode
|
|
2221
|
+
*/
|
|
2222
|
+
class SuperellipseModeSvgLayer extends SuperellipseMode {
|
|
2223
|
+
|
|
2224
|
+
/**
|
|
2225
|
+
* Хранилище ссылок на созданные виртуальные DOM-элементы (svg, div, path и т.д.).
|
|
2226
|
+
* @since 1.0.0
|
|
2227
|
+
* @type {Object<string, Element>}
|
|
2228
|
+
* @protected
|
|
2229
|
+
*/
|
|
2230
|
+
_virtualElementList = {};
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* Текущая строка viewBox для SVG.
|
|
2234
|
+
* @since 1.0.0
|
|
2235
|
+
* @type {string}
|
|
2236
|
+
* @protected
|
|
2237
|
+
*/
|
|
2238
|
+
_viewbox;
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* =============================================================
|
|
2243
|
+
* PUBLIC
|
|
2244
|
+
* =============================================================
|
|
2245
|
+
*/
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
/**
|
|
2249
|
+
* Создаёт экземпляр режима svg-layer.
|
|
2250
|
+
* @since 1.0.0
|
|
2251
|
+
* @param {Element} element - Целевой DOM-элемент.
|
|
2252
|
+
* @param {boolean} [debug=false] - Флаг отладки.
|
|
2253
|
+
*/
|
|
2254
|
+
constructor(element, debug = false) {
|
|
2255
|
+
super(element, debug);
|
|
2256
|
+
|
|
2257
|
+
this._initViewbox();
|
|
2258
|
+
this._initVirtualElementList();
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
|
|
2262
|
+
/**
|
|
2263
|
+
* =============================================================
|
|
2264
|
+
* PRIVATE
|
|
2265
|
+
* =============================================================
|
|
2266
|
+
*/
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* Выполняет обновление: применяет стили к слою div и обновляет путь.
|
|
2271
|
+
* @since 1.0.0
|
|
2272
|
+
* @override
|
|
2273
|
+
* @protected
|
|
2274
|
+
* @returns {void}
|
|
2275
|
+
*/
|
|
2276
|
+
_executeUpdate() {
|
|
2277
|
+
this._applyCurrentInlineVirtualSvgLayerStyles();
|
|
2278
|
+
this._applyCurrentCurve();
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* Возвращает имя режима ('svg-layer').
|
|
2283
|
+
* @since 1.0.0
|
|
2284
|
+
* @override
|
|
2285
|
+
* @protected
|
|
2286
|
+
* @returns {string}
|
|
2287
|
+
*/
|
|
2288
|
+
_getModeName() {
|
|
2289
|
+
return 'svg-layer';
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Возвращает стили, применяемые к основному элементу при активации.
|
|
2294
|
+
* @since 1.0.0
|
|
2295
|
+
* @override
|
|
2296
|
+
* @protected
|
|
2297
|
+
* @returns {Object<string, string>}
|
|
2298
|
+
*/
|
|
2299
|
+
_getActivatedStyles() {
|
|
2300
|
+
return {
|
|
2301
|
+
'background': 'none',
|
|
2302
|
+
'border-color': 'transparent',
|
|
2303
|
+
'border-width': '',
|
|
2304
|
+
'border-width': '0px',
|
|
2305
|
+
// 'border-style': 'none',
|
|
2306
|
+
// 'border': 'unset',
|
|
2307
|
+
'border-radius': '0px',
|
|
2308
|
+
'box-shadow': 'unset',
|
|
2309
|
+
'position': 'relative',
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* Возвращает стили для SVG-контейнера.
|
|
2315
|
+
* @since 1.0.0
|
|
2316
|
+
* @protected
|
|
2317
|
+
* @returns {Object<string, string>}
|
|
2318
|
+
*/
|
|
2319
|
+
_getSvgLayerStyles() {
|
|
2320
|
+
return {
|
|
2321
|
+
'position': 'absolute',
|
|
2322
|
+
'top': '0px',
|
|
2323
|
+
'left': '0px',
|
|
2324
|
+
'width': '100%',
|
|
2325
|
+
'height': '100%',
|
|
2326
|
+
'pointer-events': 'none'
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/**
|
|
2331
|
+
* Возвращает список CSS-свойств, которые переносятся во внутренний div.
|
|
2332
|
+
* @since 1.0.0
|
|
2333
|
+
* @protected
|
|
2334
|
+
* @returns {string[]}
|
|
2335
|
+
*/
|
|
2336
|
+
_getSvgLayerDivProps() {
|
|
2337
|
+
return [
|
|
2338
|
+
// 'color',
|
|
2339
|
+
'background',
|
|
2340
|
+
// 'background-size',
|
|
2341
|
+
// 'background-position',
|
|
2342
|
+
'box-shadow'
|
|
2343
|
+
];
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* =============================================================
|
|
2349
|
+
* STYLES
|
|
2350
|
+
* =============================================================
|
|
2351
|
+
*/
|
|
2352
|
+
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* Применяет инлайновые стили к указанному элементу.
|
|
2356
|
+
* @since 1.0.0
|
|
2357
|
+
* @protected
|
|
2358
|
+
* @param {Object<string, string>} props - Объект стилей.
|
|
2359
|
+
* @param {HTMLElement|SVGElement} element - Целевой элемент.
|
|
2360
|
+
* @returns {void}
|
|
2361
|
+
*/
|
|
2362
|
+
_applyInlineStyles(props, element) {
|
|
2363
|
+
const managedProperties = this._getManagedProperties();
|
|
2364
|
+
for(const prop of managedProperties) {
|
|
2365
|
+
const inlineValue = props[prop];
|
|
2366
|
+
const currentValue = element.style.getPropertyValue(prop);
|
|
2367
|
+
if (inlineValue !== undefined) {
|
|
2368
|
+
if (currentValue !== inlineValue) {
|
|
2369
|
+
element.style.setProperty(prop, inlineValue);
|
|
2370
|
+
}
|
|
2371
|
+
} else {
|
|
2372
|
+
if (currentValue !== '') {
|
|
2373
|
+
element.style.removeProperty(prop);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Применяет все стили (div, border, shadows) к виртуальным слоям, если режим активен.
|
|
2381
|
+
* @since 1.0.0
|
|
2382
|
+
* @protected
|
|
2383
|
+
* @returns {void}
|
|
2384
|
+
*/
|
|
2385
|
+
_applyCurrentInlineVirtualSvgLayerStyles() {
|
|
2386
|
+
if ( this.isActivated() ) {
|
|
2387
|
+
this._applyCurrentInlineVirtualSvgLayerDivStyles();
|
|
2388
|
+
this._applyCurrentInlineVirtualSvgLayerBorderStyles();
|
|
2389
|
+
this._applyCurrentInlineVirtualSvgLayerShadowsStyles();
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
|
|
2394
|
+
/**
|
|
2395
|
+
* Применяет стили к виртуальному div-слою.
|
|
2396
|
+
* @since 1.0.0
|
|
2397
|
+
* @protected
|
|
2398
|
+
* @returns {void}
|
|
2399
|
+
*/
|
|
2400
|
+
_applyCurrentInlineVirtualSvgLayerDivStyles() {
|
|
2401
|
+
const inlineSvgLayerDivStyles = this._getCurrentInlineVirtualSvgLayerDivStyles();
|
|
2402
|
+
const svgLayerDiv = this._virtualElementList.svgLayerDiv;
|
|
2403
|
+
this._applyInlineStyles(inlineSvgLayerDivStyles, svgLayerDiv);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
/**
|
|
2407
|
+
* Возвращает стили для внутреннего div-слоя, извлечённые из вычисленных стилей элемента.
|
|
2408
|
+
* @since 1.0.0
|
|
2409
|
+
* @protected
|
|
2410
|
+
* @returns {Object<string, string>}
|
|
2411
|
+
*/
|
|
2412
|
+
_getCurrentInlineVirtualSvgLayerDivStyles() {
|
|
2413
|
+
// jsse_console.debug(this._element, '[MODE SvgLayer]', '_getCurrentInlineVirtualSvgLayerDivStyles()');
|
|
2414
|
+
const result = {};
|
|
2415
|
+
const svgLayerDivProps = this._getSvgLayerDivProps();
|
|
2416
|
+
for (const prop of svgLayerDivProps) {
|
|
2417
|
+
const value = this._getComputedProp(prop);
|
|
2418
|
+
if (value !== undefined) {
|
|
2419
|
+
result[prop] = value;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
return result;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
/**
|
|
2426
|
+
* Применяет стили границы (цвет, толщину, стиль) к SVG-элементу `border`.
|
|
2427
|
+
* @since 1.0.0
|
|
2428
|
+
* @protected
|
|
2429
|
+
* @returns {void}
|
|
2430
|
+
*/
|
|
2431
|
+
_applyCurrentInlineVirtualSvgLayerBorderStyles() {
|
|
2432
|
+
const svgLayerBorder = this._virtualElementList.svgLayerBorder;
|
|
2433
|
+
const borderColor = this._getComputedProp('border-color');
|
|
2434
|
+
svgLayerBorder.setAttribute('stroke', borderColor);
|
|
2435
|
+
const borderWidth = this._getComputedProp('border-width');
|
|
2436
|
+
const borderWidthNumber = borderWidth ? (parseFloat(borderWidth) * 2) : 0;
|
|
2437
|
+
svgLayerBorder.setAttribute('stroke-width', borderWidthNumber);
|
|
2438
|
+
const borderStyle = this._getComputedProp('border-style');
|
|
2439
|
+
this._applyBorderStyleToStroke(borderStyle, svgLayerBorder);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/**
|
|
2443
|
+
* Устанавливает атрибут `stroke-dasharray` для элемента пути.
|
|
2444
|
+
* @since 1.0.0
|
|
2445
|
+
* @protected
|
|
2446
|
+
* @param {SVGElement} pathElement - SVG-элемент (обычно path или use).
|
|
2447
|
+
* @param {string} value - Значение dasharray.
|
|
2448
|
+
* @returns {void}
|
|
2449
|
+
*/
|
|
2450
|
+
_setStrokeDasharray(pathElement, value) {
|
|
2451
|
+
pathElement.setAttribute('stroke-dasharray', value);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Устанавливает атрибут `stroke-linecap` для элемента пути.
|
|
2456
|
+
* @since 1.0.0
|
|
2457
|
+
* @protected
|
|
2458
|
+
* @param {SVGElement} pathElement - SVG-элемент.
|
|
2459
|
+
* @param {string} value - Значение linecap (butt, round, square).
|
|
2460
|
+
* @returns {void}
|
|
2461
|
+
*/
|
|
2462
|
+
_setStrokeLinecap(pathElement, value) {
|
|
2463
|
+
pathElement.setAttribute('stroke-linecap', value);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* Устанавливает атрибут `stroke-opacity` для элемента пути.
|
|
2468
|
+
* @since 1.0.0
|
|
2469
|
+
* @protected
|
|
2470
|
+
* @param {SVGElement} pathElement - SVG-элемент.
|
|
2471
|
+
* @param {string|number} value - Прозрачность (0..1).
|
|
2472
|
+
* @returns {void}
|
|
2473
|
+
*/
|
|
2474
|
+
_setStrokeOpacity(pathElement, value) {
|
|
2475
|
+
pathElement.setAttribute('stroke-opacity', value);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Преобразует CSS-стиль границы в атрибуты SVG-элемента.
|
|
2480
|
+
* @since 1.0.0
|
|
2481
|
+
* @protected
|
|
2482
|
+
* @param {string} borderStyle - Стиль границы (solid, dotted, dashed и т.д.).
|
|
2483
|
+
* @param {SVGElement} pathElement - Элемент, к которому применяется обводка.
|
|
2484
|
+
* @returns {void}
|
|
2485
|
+
*/
|
|
2486
|
+
_applyBorderStyleToStroke(borderStyle, pathElement) {
|
|
2487
|
+
/** Сброс атрибутов **/
|
|
2488
|
+
pathElement.removeAttribute('stroke-dasharray');
|
|
2489
|
+
pathElement.removeAttribute('stroke-linecap');
|
|
2490
|
+
pathElement.removeAttribute('stroke-linejoin');
|
|
2491
|
+
|
|
2492
|
+
switch(borderStyle) {
|
|
2493
|
+
case 'solid':
|
|
2494
|
+
/** Сплошная линия (значения по умолчанию) **/
|
|
2495
|
+
break;
|
|
2496
|
+
|
|
2497
|
+
case 'dotted':
|
|
2498
|
+
this._setStrokeDasharray(pathElement, '0, 8');
|
|
2499
|
+
this._setStrokeLinecap(pathElement, 'round');
|
|
2500
|
+
break;
|
|
2501
|
+
|
|
2502
|
+
case 'dashed':
|
|
2503
|
+
this._setStrokeDasharray(pathElement, '10, 6');
|
|
2504
|
+
break;
|
|
2505
|
+
|
|
2506
|
+
case 'double':
|
|
2507
|
+
/** Для double нужно два отдельных пути или фильтр **/
|
|
2508
|
+
jsse_console.warn({label:'MODE SVG LAYER',element:this._element}, '«border-style: double» is not supported');
|
|
2509
|
+
break;
|
|
2510
|
+
|
|
2511
|
+
case 'groove':
|
|
2512
|
+
this._setStrokeDasharray(pathElement, '1, 2');
|
|
2513
|
+
this._setStrokeLinecap(pathElement, 'round');
|
|
2514
|
+
break;
|
|
2515
|
+
|
|
2516
|
+
case 'ridge':
|
|
2517
|
+
this._setStrokeDasharray(pathElement, '3, 3');
|
|
2518
|
+
break;
|
|
2519
|
+
|
|
2520
|
+
case 'inset':
|
|
2521
|
+
/** Имитация inset через полупрозрачность **/
|
|
2522
|
+
this._setStrokeOpacity(pathElement, '0.7');
|
|
2523
|
+
break;
|
|
2524
|
+
|
|
2525
|
+
case 'outset':
|
|
2526
|
+
this._setStrokeOpacity(pathElement, '0.5');
|
|
2527
|
+
break;
|
|
2528
|
+
|
|
2529
|
+
case 'dash-dot':
|
|
2530
|
+
/** Кастомный стиль **/
|
|
2531
|
+
this._setStrokeDasharray(pathElement, '15, 5, 5, 5');
|
|
2532
|
+
break;
|
|
2533
|
+
|
|
2534
|
+
case 'dash-dot-dot':
|
|
2535
|
+
/** Кастомный стиль **/
|
|
2536
|
+
this._setStrokeDasharray(pathElement, '15, 5, 5, 5, 5, 5');
|
|
2537
|
+
break;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
/**
|
|
2542
|
+
* Применяет тени (`box-shadow`) к SVG-слою, создавая фильтры.
|
|
2543
|
+
* @since 1.0.0
|
|
2544
|
+
* @protected
|
|
2545
|
+
* @returns {void}
|
|
2546
|
+
*/
|
|
2547
|
+
_applyCurrentInlineVirtualSvgLayerShadowsStyles() {
|
|
2548
|
+
const boxShadowValue = this._getComputedProp('box-shadow');
|
|
2549
|
+
const shadows = this._parseBoxShadow(boxShadowValue);
|
|
2550
|
+
|
|
2551
|
+
const svg = this._virtualElementList.svgLayer;
|
|
2552
|
+
const gFilters = this._virtualElementList.svgLayerGFilters;
|
|
2553
|
+
const gShadows = this._virtualElementList.svgLayerGShadows;
|
|
2554
|
+
const path = this._virtualElementList.svgLayerPath;
|
|
2555
|
+
|
|
2556
|
+
gFilters.replaceChildren();
|
|
2557
|
+
gShadows.replaceChildren();
|
|
2558
|
+
|
|
2559
|
+
const id = svg.getAttribute('id');
|
|
2560
|
+
const pathId = path.getAttribute('id');
|
|
2561
|
+
|
|
2562
|
+
for (let i = 0; i < shadows.length; i++) {
|
|
2563
|
+
|
|
2564
|
+
if (shadows[i].inset) continue;
|
|
2565
|
+
|
|
2566
|
+
const shadowValues = shadows[i];
|
|
2567
|
+
shadowValues.spreadRadius;
|
|
2568
|
+
|
|
2569
|
+
const filter = this._createVirtualSvgElement('filter');
|
|
2570
|
+
const feGaussianBlur = this._createVirtualSvgElement('feGaussianBlur');
|
|
2571
|
+
const feOffset = this._createVirtualSvgElement('feOffset');
|
|
2572
|
+
const feFlood = this._createVirtualSvgElement('feFlood');
|
|
2573
|
+
const feComposite = this._createVirtualSvgElement('feComposite');
|
|
2574
|
+
const shadow = this._createVirtualSvgElement('use');
|
|
2575
|
+
|
|
2576
|
+
const filterId = `${id}__filter_${i}`;
|
|
2577
|
+
const filterBlurId = `${filterId}__blur`;
|
|
2578
|
+
const filterOffsetId = `${filterId}__offset`;
|
|
2579
|
+
const filterColorId = `${filterId}__color`;
|
|
2580
|
+
const filterShadowId = `${filterId}__shadow`;
|
|
2581
|
+
const shadowId = `${id}__shadow_${i}`;
|
|
2582
|
+
|
|
2583
|
+
gFilters.appendChild(filter);
|
|
2584
|
+
filter.setAttribute('id', filterId);
|
|
2585
|
+
filter.setAttribute('x', '-100%');
|
|
2586
|
+
filter.setAttribute('y', '-100%');
|
|
2587
|
+
filter.setAttribute('width', '300%');
|
|
2588
|
+
filter.setAttribute('height', '300%');
|
|
2589
|
+
filter.appendChild(feGaussianBlur);
|
|
2590
|
+
filter.appendChild(feOffset);
|
|
2591
|
+
filter.appendChild(feFlood);
|
|
2592
|
+
filter.appendChild(feComposite);
|
|
2593
|
+
feGaussianBlur.setAttribute('in', 'SourceAlpha');
|
|
2594
|
+
feGaussianBlur.setAttribute('stdDeviation', shadowValues.blurRadius / 2);
|
|
2595
|
+
feGaussianBlur.setAttribute('result', filterBlurId);
|
|
2596
|
+
feOffset.setAttribute('dx', shadowValues.offsetX);
|
|
2597
|
+
feOffset.setAttribute('dy', shadowValues.offsetY);
|
|
2598
|
+
feOffset.setAttribute('in', filterBlurId);
|
|
2599
|
+
feOffset.setAttribute('result', filterOffsetId);
|
|
2600
|
+
feFlood.style.setProperty('flood-color', shadowValues.color);
|
|
2601
|
+
feFlood.setAttribute('result', filterColorId);
|
|
2602
|
+
feComposite.setAttribute('in', filterColorId);
|
|
2603
|
+
feComposite.setAttribute('in2', filterOffsetId);
|
|
2604
|
+
feComposite.setAttribute('operator', 'in');
|
|
2605
|
+
feComposite.setAttribute('result', filterShadowId);
|
|
2606
|
+
|
|
2607
|
+
gShadows.appendChild(shadow);
|
|
2608
|
+
shadow.setAttribute('href', `#${pathId}`);
|
|
2609
|
+
shadow.setAttribute('id', shadowId);
|
|
2610
|
+
shadow.setAttribute('filter', `url(#${filterId})`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* Разбирает значение `box-shadow` на массив объектов теней.
|
|
2616
|
+
* @since 1.0.0
|
|
2617
|
+
* @protected
|
|
2618
|
+
* @param {string} boxShadowValue - Строка свойства `box-shadow`.
|
|
2619
|
+
* @returns {Array<Object>} Массив теней с полями: inset, color, offsetX, offsetY, blurRadius, spreadRadius, originalColorFormat.
|
|
2620
|
+
*/
|
|
2621
|
+
_parseBoxShadow(boxShadowValue) {
|
|
2622
|
+
if (!boxShadowValue || boxShadowValue === 'none') return [];
|
|
2623
|
+
|
|
2624
|
+
/** Разделяем тени **/
|
|
2625
|
+
const shadows = [];
|
|
2626
|
+
let current = '';
|
|
2627
|
+
let depth = 0;
|
|
2628
|
+
|
|
2629
|
+
for (let char of boxShadowValue) {
|
|
2630
|
+
if (char === '(') depth++;
|
|
2631
|
+
if (char === ')') depth--;
|
|
2632
|
+
if (char === ',' && depth === 0) {
|
|
2633
|
+
shadows.push(current.trim());
|
|
2634
|
+
current = '';
|
|
2635
|
+
} else {
|
|
2636
|
+
current += char;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
shadows.push(current.trim());
|
|
2640
|
+
|
|
2641
|
+
return shadows.map(shadow => {
|
|
2642
|
+
/** Парсим одну тень **/
|
|
2643
|
+
const parts = shadow.match(/(?:rgba?\([^)]+\)|\S+)/g);
|
|
2644
|
+
if (!parts) return null;
|
|
2645
|
+
|
|
2646
|
+
const result = {
|
|
2647
|
+
inset: false,
|
|
2648
|
+
color: null,
|
|
2649
|
+
offsetX: 0,
|
|
2650
|
+
offsetY: 0,
|
|
2651
|
+
blurRadius: 0,
|
|
2652
|
+
spreadRadius: 0,
|
|
2653
|
+
originalColorFormat: null // исходный формат (уже нормализован getComputedStyle)
|
|
2654
|
+
};
|
|
2655
|
+
|
|
2656
|
+
/** Проверяем inset **/
|
|
2657
|
+
const insetIndex = parts.indexOf('inset');
|
|
2658
|
+
if (insetIndex !== -1) {
|
|
2659
|
+
result.inset = true;
|
|
2660
|
+
parts.splice(insetIndex, 1);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/** Цвет всегда будет в формате rgb/rgba после getComputedStyle **/
|
|
2664
|
+
const colorPart = parts.find(p => p.startsWith('rgb'));
|
|
2665
|
+
if (colorPart) {
|
|
2666
|
+
result.color = colorPart;
|
|
2667
|
+
result.originalColorFormat = colorPart;
|
|
2668
|
+
parts.splice(parts.indexOf(colorPart), 1);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/** Парсим числовые значения **/
|
|
2672
|
+
const numbers = parts
|
|
2673
|
+
.map(p => parseFloat(p))
|
|
2674
|
+
.filter(n => !isNaN(n));
|
|
2675
|
+
|
|
2676
|
+
if (numbers[0] !== undefined) result.offsetX = numbers[0];
|
|
2677
|
+
if (numbers[1] !== undefined) result.offsetY = numbers[1];
|
|
2678
|
+
if (numbers[2] !== undefined) result.blurRadius = numbers[2];
|
|
2679
|
+
if (numbers[3] !== undefined) result.spreadRadius = numbers[3];
|
|
2680
|
+
|
|
2681
|
+
return result;
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* =============================================================
|
|
2688
|
+
* VIRTUAL
|
|
2689
|
+
* =============================================================
|
|
2690
|
+
*/
|
|
2691
|
+
|
|
2692
|
+
|
|
2693
|
+
/**
|
|
2694
|
+
* Инициализирует список виртуальных элементов (div, svg и пр.).
|
|
2695
|
+
* @since 1.0.0
|
|
2696
|
+
* @protected
|
|
2697
|
+
* @returns {void}
|
|
2698
|
+
*/
|
|
2699
|
+
_initVirtualElementList() {
|
|
2700
|
+
this._initVirtualInnerWrapper();
|
|
2701
|
+
this._initVirtualSvgLayer();
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
/**
|
|
2705
|
+
* Создаёт SVG-элемент с указанным тегом в пространстве имён SVG.
|
|
2706
|
+
* @since 1.0.0
|
|
2707
|
+
* @protected
|
|
2708
|
+
* @param {string} tag - Имя тега (например, 'svg', 'path', 'filter').
|
|
2709
|
+
* @returns {SVGElement}
|
|
2710
|
+
*/
|
|
2711
|
+
_createVirtualSvgElement(tag) {
|
|
2712
|
+
return document.createElementNS('http://www.w3.org/2000/svg', tag);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
/**
|
|
2716
|
+
* Создаёт HTML-элемент с указанным тегом.
|
|
2717
|
+
* @since 1.0.0
|
|
2718
|
+
* @protected
|
|
2719
|
+
* @param {string} tag - Имя тега (например, 'div').
|
|
2720
|
+
* @returns {HTMLElement}
|
|
2721
|
+
*/
|
|
2722
|
+
_createVirtualHtmlElement(tag) {
|
|
2723
|
+
return document.createElement(tag);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/**
|
|
2727
|
+
* Создаёт SVG-элементы для слоя.
|
|
2728
|
+
* @since 1.0.0
|
|
2729
|
+
* @protected
|
|
2730
|
+
* @returns {void}
|
|
2731
|
+
*/
|
|
2732
|
+
_initVirtualSvgLayer() {
|
|
2733
|
+
if (this._virtualElementList.svgLayer) return;
|
|
2734
|
+
|
|
2735
|
+
const id = this._id;
|
|
2736
|
+
const svgId = `jsse_${id}`;
|
|
2737
|
+
const clipId = `jsse_${id}__clip`;
|
|
2738
|
+
const pathId = `jsse_${id}__path`;
|
|
2739
|
+
const filtersId = `jsse_${id}__filters`;
|
|
2740
|
+
const shadowsId = `jsse_${id}__shadows`;
|
|
2741
|
+
const divId = `jsse_${id}__div`;
|
|
2742
|
+
const borderId = `jsse_${id}__border`;
|
|
2743
|
+
|
|
2744
|
+
const svg = this._createVirtualSvgElement('svg');
|
|
2745
|
+
const defs = this._createVirtualSvgElement('defs');
|
|
2746
|
+
const clipPath = this._createVirtualSvgElement('clipPath');
|
|
2747
|
+
const path = this._createVirtualSvgElement('path');
|
|
2748
|
+
const gFilters = this._createVirtualSvgElement('g');
|
|
2749
|
+
const gShadows = this._createVirtualSvgElement('g');
|
|
2750
|
+
const html = this._createVirtualSvgElement('foreignObject');
|
|
2751
|
+
const div = this._createVirtualHtmlElement('div');
|
|
2752
|
+
const border = this._createVirtualSvgElement('use');
|
|
2753
|
+
|
|
2754
|
+
/** svg **/
|
|
2755
|
+
svg.setAttribute('id', svgId);
|
|
2756
|
+
svg.classList.add('jsse--svg-layer--bg');
|
|
2757
|
+
svg.setAttribute('viewBox', this._getViewbox());
|
|
2758
|
+
svg.setAttribute('preserveAspectRatio', 'none');
|
|
2759
|
+
const svgProps = this._getSvgLayerStyles();
|
|
2760
|
+
for (const prop in svgProps) {
|
|
2761
|
+
svg.style.setProperty(prop, svgProps[prop]);
|
|
2762
|
+
}
|
|
2763
|
+
svg.style.setProperty('overflow', 'visible');
|
|
2764
|
+
svg.appendChild(defs);
|
|
2765
|
+
svg.appendChild(gShadows);
|
|
2766
|
+
svg.appendChild(html);
|
|
2767
|
+
svg.appendChild(border);
|
|
2768
|
+
/** svg > defs **/
|
|
2769
|
+
defs.appendChild(clipPath);
|
|
2770
|
+
defs.appendChild(gFilters);
|
|
2771
|
+
/** svg > defs > clipPath **/
|
|
2772
|
+
clipPath.setAttribute('id', clipId);
|
|
2773
|
+
clipPath.appendChild(path);
|
|
2774
|
+
/** svg > defs > clipPath > path **/
|
|
2775
|
+
path.setAttribute('id', pathId);
|
|
2776
|
+
path.setAttribute('d', ''); // только создаем слой, не заполняем
|
|
2777
|
+
/** svg > defs > g **/
|
|
2778
|
+
gFilters.setAttribute('id', filtersId);
|
|
2779
|
+
/** svg > g **/
|
|
2780
|
+
gShadows.setAttribute('id', shadowsId);
|
|
2781
|
+
/** svg > foreignObject **/
|
|
2782
|
+
html.setAttribute('id', shadowsId);
|
|
2783
|
+
html.setAttribute('width', '100%');
|
|
2784
|
+
html.setAttribute('height', '100%');
|
|
2785
|
+
html.setAttribute('clip-path', `url(#${clipId})`);
|
|
2786
|
+
html.appendChild(div);
|
|
2787
|
+
/** svg > foreignObject > div **/
|
|
2788
|
+
div.setAttribute('id', divId);
|
|
2789
|
+
div.style.setProperty('width', '100%');
|
|
2790
|
+
div.style.setProperty('height', '100%');
|
|
2791
|
+
/** svg > border **/
|
|
2792
|
+
border.setAttribute('id', borderId);
|
|
2793
|
+
border.setAttribute('href', `#${pathId}`);
|
|
2794
|
+
border.setAttribute('fill', 'none');
|
|
2795
|
+
border.setAttribute('stroke', '');
|
|
2796
|
+
border.setAttribute('stroke-width', '0');
|
|
2797
|
+
border.setAttribute('clip-path', `url(#${clipId})`);
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
this._virtualElementList.svgLayer = svg;
|
|
2801
|
+
this._virtualElementList.svgLayerPath = path;
|
|
2802
|
+
this._virtualElementList.svgLayerGFilters = gFilters;
|
|
2803
|
+
this._virtualElementList.svgLayerGShadows = gShadows;
|
|
2804
|
+
this._virtualElementList.svgLayerDiv = div;
|
|
2805
|
+
this._virtualElementList.svgLayerBorder = border;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
/**
|
|
2809
|
+
* Создаёт внутренний div-обёртку для контента.
|
|
2810
|
+
* @since 1.0.0
|
|
2811
|
+
* @protected
|
|
2812
|
+
* @returns {void}
|
|
2813
|
+
*/
|
|
2814
|
+
_initVirtualInnerWrapper() {
|
|
2815
|
+
if (this._virtualElementList.innerWrapper) return;
|
|
2816
|
+
|
|
2817
|
+
const innerWrapper = this._createVirtualHtmlElement('div');
|
|
2818
|
+
innerWrapper.className = 'jsse--svg-layer--content';
|
|
2819
|
+
innerWrapper.style.setProperty('position', 'relative');
|
|
2820
|
+
|
|
2821
|
+
this._virtualElementList.innerWrapper = innerWrapper;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
/**
|
|
2825
|
+
* Добавляет виртуальные элементы в DOM.
|
|
2826
|
+
* @override
|
|
2827
|
+
* @since 1.0.0
|
|
2828
|
+
* @protected
|
|
2829
|
+
* @returns {void}
|
|
2830
|
+
*/
|
|
2831
|
+
_appendVirtualElements() {
|
|
2832
|
+
this._appendInnerWrapper();
|
|
2833
|
+
this._appendSvgLayer();
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
/**
|
|
2837
|
+
* Удаляет виртуальные элементы из DOM.
|
|
2838
|
+
* @override
|
|
2839
|
+
* @since 1.0.0
|
|
2840
|
+
* @protected
|
|
2841
|
+
* @returns {void}
|
|
2842
|
+
*/
|
|
2843
|
+
_removeVirtualElements() {
|
|
2844
|
+
this._removeSvgLayer();
|
|
2845
|
+
this._removeInnerWrapper();
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
/**
|
|
2849
|
+
* Добавляет SVG-слой в начало элемента.
|
|
2850
|
+
* @since 1.0.0
|
|
2851
|
+
* @protected
|
|
2852
|
+
* @returns {void}
|
|
2853
|
+
*/
|
|
2854
|
+
_appendSvgLayer() {
|
|
2855
|
+
const svgLayer = this._virtualElementList.svgLayer;
|
|
2856
|
+
// TODO: нужна ли проверка на наличие svgLayer у this._element?
|
|
2857
|
+
this._element.insertBefore(svgLayer, this._element.firstChild);
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/**
|
|
2861
|
+
* Удаляет SVG-слой.
|
|
2862
|
+
* @since 1.0.0
|
|
2863
|
+
* @protected
|
|
2864
|
+
* @returns {void}
|
|
2865
|
+
*/
|
|
2866
|
+
_removeSvgLayer() {
|
|
2867
|
+
const svgLayer = this._virtualElementList.svgLayer;
|
|
2868
|
+
if (svgLayer && svgLayer.parentNode === this._element) {
|
|
2869
|
+
this._element.removeChild(svgLayer);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
/**
|
|
2874
|
+
* Перемещает дочерние элементы элемента во внутреннюю обёртку.
|
|
2875
|
+
* @since 1.0.0
|
|
2876
|
+
* @protected
|
|
2877
|
+
* @returns {void}
|
|
2878
|
+
*/
|
|
2879
|
+
_appendInnerWrapper() {
|
|
2880
|
+
const innerWrapper = this._virtualElementList.innerWrapper;
|
|
2881
|
+
|
|
2882
|
+
/** Перемещаем внутренние элемены this._element в innerWrapper **/
|
|
2883
|
+
const children = Array.from(this._element.childNodes);
|
|
2884
|
+
for (const child of children) {
|
|
2885
|
+
innerWrapper.appendChild(child);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
this._element.appendChild(innerWrapper);
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
/**
|
|
2892
|
+
* Возвращает дочерние элементы из обёртки обратно в элемент.
|
|
2893
|
+
* @since 1.0.0
|
|
2894
|
+
* @protected
|
|
2895
|
+
* @returns {void}
|
|
2896
|
+
*/
|
|
2897
|
+
_removeInnerWrapper() {
|
|
2898
|
+
const innerWrapper = this._virtualElementList.innerWrapper;
|
|
2899
|
+
|
|
2900
|
+
/** Перемещаем внутренние элемены innerWrapper в this._element **/
|
|
2901
|
+
const children = Array.from(innerWrapper.childNodes);
|
|
2902
|
+
for (const child of children) {
|
|
2903
|
+
this._element.appendChild(child);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
this._element.removeChild(innerWrapper);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
|
|
2911
|
+
/**
|
|
2912
|
+
* =============================================================
|
|
2913
|
+
* CURVE
|
|
2914
|
+
* =============================================================
|
|
2915
|
+
*/
|
|
2916
|
+
|
|
2917
|
+
|
|
2918
|
+
/**
|
|
2919
|
+
* Инициализирует viewBox.
|
|
2920
|
+
* @since 1.0.0
|
|
2921
|
+
* @protected
|
|
2922
|
+
* @returns {void}
|
|
2923
|
+
*/
|
|
2924
|
+
_initViewbox() {
|
|
2925
|
+
this._recalculateViewbox();
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Пересчитывает viewBox при изменении размеров.
|
|
2930
|
+
* @override
|
|
2931
|
+
* @since 1.0.0
|
|
2932
|
+
* @protected
|
|
2933
|
+
* @returns {void}
|
|
2934
|
+
*/
|
|
2935
|
+
_recalculateCurve() {
|
|
2936
|
+
super._recalculateCurve();
|
|
2937
|
+
|
|
2938
|
+
this._recalculateViewbox();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/**
|
|
2942
|
+
* Обновляет viewBox на основе текущих размеров.
|
|
2943
|
+
* @since 1.0.0
|
|
2944
|
+
* @protected
|
|
2945
|
+
* @returns {void}
|
|
2946
|
+
*/
|
|
2947
|
+
_recalculateViewbox() {
|
|
2948
|
+
if ( this._size.width > 0 && this._size.height > 0 ) {
|
|
2949
|
+
this._viewbox = `0 0 ${this._size.width} ${this._size.height}`;
|
|
2950
|
+
} else {
|
|
2951
|
+
this._viewbox = '0 0 0 0'; // Сбрасываем путь
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Возвращает строку viewBox.
|
|
2957
|
+
* @since 1.0.0
|
|
2958
|
+
* @protected
|
|
2959
|
+
* @returns {string}
|
|
2960
|
+
*/
|
|
2961
|
+
_getViewbox() {
|
|
2962
|
+
return this._viewbox;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
/**
|
|
2966
|
+
* Применяет кривую, обновляя viewBox и d-атрибут пути.
|
|
2967
|
+
* @override
|
|
2968
|
+
* @since 1.0.0
|
|
2969
|
+
* @protected
|
|
2970
|
+
* @returns {void}
|
|
2971
|
+
*/
|
|
2972
|
+
_applyCurve() {
|
|
2973
|
+
const svgLayer = this._virtualElementList.svgLayer;
|
|
2974
|
+
svgLayer.setAttribute('viewBox', this._getViewbox());
|
|
2975
|
+
|
|
2976
|
+
const svgLayerPath = this._virtualElementList.svgLayerPath;
|
|
2977
|
+
if (this._path) {
|
|
2978
|
+
svgLayerPath.setAttribute('d', this._path);
|
|
2979
|
+
} else {
|
|
2980
|
+
svgLayerPath.setAttribute('d', '');
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
/**
|
|
2985
|
+
* Восстанавливает исходный путь и очищает d-атрибут.
|
|
2986
|
+
* @override
|
|
2987
|
+
* @since 1.0.0
|
|
2988
|
+
* @protected
|
|
2989
|
+
* @returns {void}
|
|
2990
|
+
*/
|
|
2991
|
+
_restoreCurve() {
|
|
2992
|
+
const svgLayerPath = this._virtualElementList.svgLayerPath;
|
|
2993
|
+
|
|
2994
|
+
svgLayerPath.setAttribute('d', '');
|
|
2995
|
+
|
|
2996
|
+
super._restoreCurve();
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
|
|
3000
|
+
/**
|
|
3001
|
+
* =============================================================
|
|
3002
|
+
*
|
|
3003
|
+
* =============================================================
|
|
3004
|
+
*/
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
/**
|
|
3008
|
+
* @file src/mode-clip-path.js
|
|
3009
|
+
*
|
|
3010
|
+
* @module sj-superellipse/mode-clip-path
|
|
3011
|
+
* @since 1.0.0
|
|
3012
|
+
* @author f4n70m
|
|
3013
|
+
*
|
|
3014
|
+
* @description
|
|
3015
|
+
* Режим `clip-path` – самый лёгкий способ применения суперэллипса. Использует CSS-свойство `clip-path`
|
|
3016
|
+
* для обрезки элемента по форме суперэллипса. Не требует создания дополнительных DOM-узлов,
|
|
3017
|
+
* но не поддерживает корректное отображение теней (`box-shadow`), границ и сложных фонов.
|
|
3018
|
+
*
|
|
3019
|
+
* @extends SuperellipseMode
|
|
3020
|
+
* @example
|
|
3021
|
+
* const mode = new SuperellipseModeClipPath(element);
|
|
3022
|
+
* mode.activate();
|
|
3023
|
+
*/
|
|
3024
|
+
|
|
3025
|
+
|
|
3026
|
+
|
|
3027
|
+
/**
|
|
3028
|
+
* Режим, использующий CSS-свойство `clip-path` для обрезки элемента.
|
|
3029
|
+
* Не требует создания дополнительных DOM-узлов, но не поддерживает тени, границы и сложные фоны.
|
|
3030
|
+
* @class SuperellipseModeClipPath
|
|
3031
|
+
* @extends SuperellipseMode
|
|
3032
|
+
* @since 1.0.0
|
|
3033
|
+
*/
|
|
3034
|
+
class SuperellipseModeClipPath extends SuperellipseMode {
|
|
3035
|
+
|
|
3036
|
+
|
|
3037
|
+
/**
|
|
3038
|
+
* =============================================================
|
|
3039
|
+
* PUBLIC
|
|
3040
|
+
* =============================================================
|
|
3041
|
+
*/
|
|
3042
|
+
|
|
3043
|
+
/**
|
|
3044
|
+
* Создаёт экземпляр режима clip-path.
|
|
3045
|
+
* @since 1.0.0
|
|
3046
|
+
* @param {Element} element - Целевой DOM-элемент.
|
|
3047
|
+
* @param {boolean} [debug=false] - Флаг отладки (передаётся в родительский класс).
|
|
3048
|
+
* @returns {SuperellipseModeClipPath} Экземпляр режима.
|
|
3049
|
+
*/
|
|
3050
|
+
constructor(element, debug = false) {
|
|
3051
|
+
super(element, debug);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
|
|
3055
|
+
/**
|
|
3056
|
+
* =============================================================
|
|
3057
|
+
*
|
|
3058
|
+
* =============================================================
|
|
3059
|
+
*/
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* @file src/controller.js
|
|
3064
|
+
*
|
|
3065
|
+
* @module sj-superellipse/controller
|
|
3066
|
+
* @since 1.0.0
|
|
3067
|
+
* @author f4n70m
|
|
3068
|
+
*
|
|
3069
|
+
* @description
|
|
3070
|
+
* Класс `SuperellipseController` – основной контроллер, связывающий DOM-элемент с выбранным режимом
|
|
3071
|
+
* (`svg-layer` или `clip-path`). Отслеживает изменения размеров, стилей, атрибутов и удаление элемента
|
|
3072
|
+
* через `ResizeObserver`, `MutationObserver`, `IntersectionObserver`. Управляет кэшированием стилей,
|
|
3073
|
+
* переключением режимов, активацией/деактивацией эффекта.
|
|
3074
|
+
*
|
|
3075
|
+
* @example
|
|
3076
|
+
* const controller = new SuperellipseController(element, { mode: 'svg-layer' });
|
|
3077
|
+
* controller.setCurveFactor(0.9);
|
|
3078
|
+
* controller.enable();
|
|
3079
|
+
*/
|
|
3080
|
+
|
|
3081
|
+
|
|
3082
|
+
|
|
3083
|
+
/**
|
|
3084
|
+
* Контроллер, управляющий применением суперэллипса к DOM-элементу.
|
|
3085
|
+
*/
|
|
3086
|
+
class SuperellipseController
|
|
3087
|
+
{
|
|
3088
|
+
_id;
|
|
3089
|
+
_debug;
|
|
3090
|
+
|
|
3091
|
+
_mode;
|
|
3092
|
+
_element;
|
|
3093
|
+
|
|
3094
|
+
_precision; // Количество знаков после запятой
|
|
3095
|
+
_curveFactor;
|
|
3096
|
+
|
|
3097
|
+
_mutationFrame;
|
|
3098
|
+
_resizeFrame;
|
|
3099
|
+
_intersectionFrame;
|
|
3100
|
+
|
|
3101
|
+
|
|
3102
|
+
_prepareTimer;
|
|
3103
|
+
_executeTimer;
|
|
3104
|
+
_isSelfMutation = false;
|
|
3105
|
+
|
|
3106
|
+
_resizeObserver;
|
|
3107
|
+
_mutationObserver;
|
|
3108
|
+
_removalObserver;
|
|
3109
|
+
_intersectionObserver;
|
|
3110
|
+
|
|
3111
|
+
_targetTriggers;
|
|
3112
|
+
_hoverHandlers;
|
|
3113
|
+
|
|
3114
|
+
_needsUpdate;
|
|
3115
|
+
_isSelfApply;
|
|
3116
|
+
|
|
3117
|
+
|
|
3118
|
+
_eventHandlers;
|
|
3119
|
+
|
|
3120
|
+
|
|
3121
|
+
/**
|
|
3122
|
+
* =============================================================
|
|
3123
|
+
* PUBLIC
|
|
3124
|
+
* =============================================================
|
|
3125
|
+
*/
|
|
3126
|
+
|
|
3127
|
+
|
|
3128
|
+
/**
|
|
3129
|
+
* Создаёт экземпляр контроллера.
|
|
3130
|
+
* @since 1.0.0
|
|
3131
|
+
* @param {Element} element - Целевой DOM-элемент.
|
|
3132
|
+
* @param {Object} [options] - Опции инициализации.
|
|
3133
|
+
* @param {boolean} [options.force] - Принудительное пересоздание, если контроллер уже существует.
|
|
3134
|
+
* @param {string} [options.mode='svg-layer'] - Режим работы: 'svg-layer' или 'clip-path'.
|
|
3135
|
+
* @param {number} [options.curveFactor] - Коэффициент кривизны (диапазон -2..2).
|
|
3136
|
+
* @param {number} [options.precision=2] - Количество знаков после запятой в координатах пути.
|
|
3137
|
+
* @param {boolean} [options.debug=false] - Включить отладочный вывод.
|
|
3138
|
+
* @returns {SuperellipseController} Экземпляр контроллера.
|
|
3139
|
+
*/
|
|
3140
|
+
constructor(element, options = {}) {
|
|
3141
|
+
|
|
3142
|
+
this._initId();
|
|
3143
|
+
|
|
3144
|
+
/** Проверка существующего контроллера **/
|
|
3145
|
+
if (this._inControllers() && !options.force) {
|
|
3146
|
+
jsse_console.warn({label:'CONTROLLER', element: element}, 'The element is already initialized. Use {force:true} to recreate it.');
|
|
3147
|
+
return this._getController();
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
this._element = element;
|
|
3151
|
+
|
|
3152
|
+
/** Default options **/
|
|
3153
|
+
const settings = {
|
|
3154
|
+
mode: options.mode ?? 'svg-layer',
|
|
3155
|
+
debug: options.debug ?? false,
|
|
3156
|
+
curveFactor: options.curveFactor ?? jsse_getBorderRadiusFactor(),
|
|
3157
|
+
precision: options.precision ?? 2
|
|
3158
|
+
};
|
|
3159
|
+
|
|
3160
|
+
this._initDebug(settings.debug);
|
|
3161
|
+
|
|
3162
|
+
jsse_console.debug({label:'CONTROLLER',element:this._element}, '[SETTINGS]', settings);
|
|
3163
|
+
|
|
3164
|
+
|
|
3165
|
+
this._curveFactor = settings.curveFactor;
|
|
3166
|
+
this._precision = settings.precision;
|
|
3167
|
+
|
|
3168
|
+
|
|
3169
|
+
this._needsUpdate = false;
|
|
3170
|
+
this._isSelfApply = false;
|
|
3171
|
+
|
|
3172
|
+
/** Слушатели **/
|
|
3173
|
+
this._resizeObserver = null;
|
|
3174
|
+
this._mutationObserver = null;
|
|
3175
|
+
this._removalObserver = null;
|
|
3176
|
+
this._intersectionObserver = null;
|
|
3177
|
+
|
|
3178
|
+
/** init **/
|
|
3179
|
+
this._initEvents();
|
|
3180
|
+
this._initCacheStyles();
|
|
3181
|
+
this._setInitiatedAttr();
|
|
3182
|
+
|
|
3183
|
+
this._setMode(settings.mode);
|
|
3184
|
+
this._activateMode();
|
|
3185
|
+
this._connectObservers();
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
/**
|
|
3189
|
+
* Переключает режим работы.
|
|
3190
|
+
* @since 1.0.0
|
|
3191
|
+
* @param {string} modeName - Имя режима ('svg-layer' или 'clip-path').
|
|
3192
|
+
* @returns {SuperellipseController} this (для цепочек вызовов).
|
|
3193
|
+
*/
|
|
3194
|
+
switchMode(modeName) {
|
|
3195
|
+
this._deactivateMode();
|
|
3196
|
+
this._unsetMode();
|
|
3197
|
+
this._setMode(modeName);
|
|
3198
|
+
this._activateMode();
|
|
3199
|
+
return this;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Проверяет, активирован ли суперэллипс.
|
|
3204
|
+
* @since 1.0.0
|
|
3205
|
+
* @returns {boolean} true, если эффект активен.
|
|
3206
|
+
*/
|
|
3207
|
+
isEnabled() {
|
|
3208
|
+
return this._mode.isActivated();
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
/**
|
|
3212
|
+
* Активирует суперэллипс.
|
|
3213
|
+
* @since 1.0.0
|
|
3214
|
+
* @returns {SuperellipseController} this.
|
|
3215
|
+
*/
|
|
3216
|
+
enable() {
|
|
3217
|
+
this._activateMode();
|
|
3218
|
+
return this;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
/**
|
|
3222
|
+
* Деактивирует суперэллипс, восстанавливая исходные стили.
|
|
3223
|
+
* @since 1.0.0
|
|
3224
|
+
* @returns {Element} Целевой элемент.
|
|
3225
|
+
*/
|
|
3226
|
+
disable() {
|
|
3227
|
+
this._deactivateMode();
|
|
3228
|
+
return this;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
/**
|
|
3232
|
+
* Устанавливает коэффициент кривизны углов.
|
|
3233
|
+
* @since 1.0.0
|
|
3234
|
+
* @param {number} value - Новое значение (диапазон -2..2).
|
|
3235
|
+
* @returns {SuperellipseController} this.
|
|
3236
|
+
*/
|
|
3237
|
+
setCurveFactor(value) {
|
|
3238
|
+
this._curveFactor = value;
|
|
3239
|
+
this._mode.updateCurveFactor(value);
|
|
3240
|
+
this._emit('update', { type: 'curveFactor' });
|
|
3241
|
+
return this;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
/**
|
|
3245
|
+
* Устанавливает точность округления координат пути.
|
|
3246
|
+
* @since 1.0.0
|
|
3247
|
+
* @param {number} value - Количество знаков после запятой.
|
|
3248
|
+
* @returns {SuperellipseController} this.
|
|
3249
|
+
*/
|
|
3250
|
+
setPrecision(value) {
|
|
3251
|
+
this._precision = value;
|
|
3252
|
+
this._mode.updatePrecision(value);
|
|
3253
|
+
return this;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Возвращает текущий SVG-путь суперэллипса.
|
|
3258
|
+
* @since 1.0.0
|
|
3259
|
+
* @returns {string} Строка с командами path.
|
|
3260
|
+
*/
|
|
3261
|
+
getPath() {
|
|
3262
|
+
return this._mode.getPath();
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
/**
|
|
3266
|
+
* Полностью уничтожает контроллер и удаляет все связанные эффекты.
|
|
3267
|
+
* @since 1.0.0
|
|
3268
|
+
* @returns {Element} Целевой элемент.
|
|
3269
|
+
*/
|
|
3270
|
+
destroy() {
|
|
3271
|
+
return this._destroyController();
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
|
|
3275
|
+
/**
|
|
3276
|
+
* =============================================================
|
|
3277
|
+
* PRIVATE
|
|
3278
|
+
* =============================================================
|
|
3279
|
+
*/
|
|
3280
|
+
|
|
3281
|
+
|
|
3282
|
+
/**
|
|
3283
|
+
* Инициализирует уникальный идентификатор контроллера.
|
|
3284
|
+
* @since 1.0.0
|
|
3285
|
+
* @protected
|
|
3286
|
+
* @returns {void}
|
|
3287
|
+
*/
|
|
3288
|
+
_initId() {
|
|
3289
|
+
this._id = jsse_counter.value;
|
|
3290
|
+
jsse_counter.increment();
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
/**
|
|
3294
|
+
* Инициализирует флаг отладки.
|
|
3295
|
+
* @since 1.0.0
|
|
3296
|
+
* @protected
|
|
3297
|
+
* @param {boolean} debug - Включить отладку.
|
|
3298
|
+
* @returns {void}
|
|
3299
|
+
*/
|
|
3300
|
+
_initDebug(debug) {
|
|
3301
|
+
this._debug = !!debug;
|
|
3302
|
+
if (this._debug) {
|
|
3303
|
+
jsse_console.set(this._element);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
/**
|
|
3308
|
+
* Проверяет, включён ли режим отладки для данного контроллера.
|
|
3309
|
+
* @since 1.0.0
|
|
3310
|
+
* @protected
|
|
3311
|
+
* @returns {boolean}
|
|
3312
|
+
*/
|
|
3313
|
+
_isDebug() {
|
|
3314
|
+
return this._debug;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* Проверяет, не скрыт ли элемент (`display: none`).
|
|
3319
|
+
* @since 1.0.0
|
|
3320
|
+
* @protected
|
|
3321
|
+
* @returns {boolean}
|
|
3322
|
+
*/
|
|
3323
|
+
_isDisplay() {
|
|
3324
|
+
const capturedStyles = getComputedStyle(this._element);
|
|
3325
|
+
return capturedStyles.getPropertyValue('display') !== 'none';
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
/**
|
|
3329
|
+
* Полное уничтожение контроллера (внутренняя логика).
|
|
3330
|
+
* @since 1.0.0
|
|
3331
|
+
* @protected
|
|
3332
|
+
* @returns {void}
|
|
3333
|
+
*/
|
|
3334
|
+
_destroyController() {
|
|
3335
|
+
this._disconnectObservers();
|
|
3336
|
+
this._deactivateMode();
|
|
3337
|
+
this._unsetMode();
|
|
3338
|
+
this._removeInitiatedAttr();
|
|
3339
|
+
|
|
3340
|
+
this._destroyStylesheet();
|
|
3341
|
+
|
|
3342
|
+
this._deleteCacheStyles();
|
|
3343
|
+
this._deleteFromControllers();
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
|
|
3347
|
+
/**
|
|
3348
|
+
* =============================================================
|
|
3349
|
+
* EVENTS API
|
|
3350
|
+
* =============================================================
|
|
3351
|
+
*/
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* Инициализирует систему событий.
|
|
3355
|
+
* @since 1.2.0
|
|
3356
|
+
* @protected
|
|
3357
|
+
* @returns {void}
|
|
3358
|
+
*/
|
|
3359
|
+
_initEvents() {
|
|
3360
|
+
this._eventHandlers = {
|
|
3361
|
+
update: [],
|
|
3362
|
+
activate: [],
|
|
3363
|
+
deactivate: [],
|
|
3364
|
+
error: []
|
|
3365
|
+
};
|
|
3366
|
+
};
|
|
3367
|
+
|
|
3368
|
+
/**
|
|
3369
|
+
* Подписывается на событие контроллера.
|
|
3370
|
+
* @since 1.2.0
|
|
3371
|
+
* @param {string} event - Имя события ('update', 'activate', 'deactivate', 'error').
|
|
3372
|
+
* @param {Function} callback - Функция-обработчик. Принимает объект события.
|
|
3373
|
+
* @returns {SuperellipseController} this.
|
|
3374
|
+
*/
|
|
3375
|
+
on(event, callback) {
|
|
3376
|
+
if (this._eventHandlers[event]) {
|
|
3377
|
+
this._eventHandlers[event].push(callback);
|
|
3378
|
+
}
|
|
3379
|
+
return this;
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
/**
|
|
3383
|
+
* Отписывается от события контроллера.
|
|
3384
|
+
* @since 1.2.0
|
|
3385
|
+
* @param {string} event - Имя события.
|
|
3386
|
+
* @param {Function} callback - Ранее добавленный обработчик.
|
|
3387
|
+
* @returns {SuperellipseController} this.
|
|
3388
|
+
*/
|
|
3389
|
+
off(event, callback) {
|
|
3390
|
+
if (this._eventHandlers[event]) {
|
|
3391
|
+
const index = this._eventHandlers[event].indexOf(callback);
|
|
3392
|
+
if (index !== -1) this._eventHandlers[event].splice(index, 1);
|
|
3393
|
+
}
|
|
3394
|
+
return this;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
/**
|
|
3398
|
+
* Вызывает событие с заданными данными.
|
|
3399
|
+
* @since 1.2.0
|
|
3400
|
+
* @protected
|
|
3401
|
+
* @param {string} event - Имя события.
|
|
3402
|
+
* @param {*} data - Данные события.
|
|
3403
|
+
* @returns {void}
|
|
3404
|
+
*/
|
|
3405
|
+
_emit(event, data) {
|
|
3406
|
+
if (this._eventHandlers[event]) {
|
|
3407
|
+
this._eventHandlers[event].forEach(cb => {
|
|
3408
|
+
try {
|
|
3409
|
+
cb({ type: event, data, timestamp: Date.now(), target: this._element });
|
|
3410
|
+
} catch (e) {
|
|
3411
|
+
console.error('[JSSE] Event handler error:', e);
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* =============================================================
|
|
3420
|
+
* MODE
|
|
3421
|
+
* =============================================================
|
|
3422
|
+
*/
|
|
3423
|
+
|
|
3424
|
+
|
|
3425
|
+
/**
|
|
3426
|
+
* Устанавливает активный режим.
|
|
3427
|
+
* @since 1.0.0
|
|
3428
|
+
* @protected
|
|
3429
|
+
* @param {string} modeName - Имя режима ('svg-layer' или 'clip-path').
|
|
3430
|
+
* @returns {void}
|
|
3431
|
+
*/
|
|
3432
|
+
_setMode(modeName) {
|
|
3433
|
+
switch (modeName) {
|
|
3434
|
+
case 'svg-layer':
|
|
3435
|
+
this._mode = new SuperellipseModeSvgLayer(this._element);
|
|
3436
|
+
break;
|
|
3437
|
+
|
|
3438
|
+
case 'clip-path':
|
|
3439
|
+
default:
|
|
3440
|
+
this._mode = new SuperellipseModeClipPath(this._element);
|
|
3441
|
+
break;
|
|
3442
|
+
}
|
|
3443
|
+
this._mode.setCurveFactor(this._curveFactor);
|
|
3444
|
+
this._mode.setPrecision(this._precision);
|
|
3445
|
+
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
/**
|
|
3449
|
+
* Удаляет текущий режим, вызывая его деструктор.
|
|
3450
|
+
* @since 1.0.0
|
|
3451
|
+
* @protected
|
|
3452
|
+
* @returns {void}
|
|
3453
|
+
*/
|
|
3454
|
+
_unsetMode() {
|
|
3455
|
+
|
|
3456
|
+
this._mode.destroy();
|
|
3457
|
+
this._mode = null;
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
/**
|
|
3461
|
+
* Активирует текущий режим и инициализирует обработчики hover.
|
|
3462
|
+
* @since 1.0.0
|
|
3463
|
+
* @protected
|
|
3464
|
+
* @returns {void}
|
|
3465
|
+
*/
|
|
3466
|
+
_activateMode() {
|
|
3467
|
+
this._mode.activate();
|
|
3468
|
+
this._initStylesheet();
|
|
3469
|
+
this._registerTargetListeners(this._targetTriggers);
|
|
3470
|
+
|
|
3471
|
+
this._emit('activate', { mode: this._mode._getModeName() });
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
/**
|
|
3475
|
+
* Деактивирует текущий режим и удаляет обработчики hover.
|
|
3476
|
+
* @since 1.0.0
|
|
3477
|
+
* @protected
|
|
3478
|
+
* @returns {void}
|
|
3479
|
+
*/
|
|
3480
|
+
_deactivateMode() {
|
|
3481
|
+
this._mode.deactivate();
|
|
3482
|
+
this._unregisterTargetListeners();
|
|
3483
|
+
|
|
3484
|
+
this._emit('deactivate', { mode: this._mode._getModeName() });
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
|
|
3488
|
+
/**
|
|
3489
|
+
* =============================================================
|
|
3490
|
+
* ATTRIBUTES
|
|
3491
|
+
* =============================================================
|
|
3492
|
+
*/
|
|
3493
|
+
|
|
3494
|
+
|
|
3495
|
+
/**
|
|
3496
|
+
* Присваивает элементу атрибут `data-jsse-initiated`.
|
|
3497
|
+
* @since 1.0.0
|
|
3498
|
+
* @protected
|
|
3499
|
+
* @returns {void}
|
|
3500
|
+
*/
|
|
3501
|
+
_setInitiatedAttr() {
|
|
3502
|
+
this._element.setAttribute('data-jsse-initiated', true);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
/**
|
|
3506
|
+
* Удаляет атрибут `data-jsse-initiated`.
|
|
3507
|
+
* @since 1.0.0
|
|
3508
|
+
* @protected
|
|
3509
|
+
* @returns {void}
|
|
3510
|
+
*/
|
|
3511
|
+
_removeInitiatedAttr() {
|
|
3512
|
+
this._element.removeAttribute('data-jsse-initiated');
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
|
|
3516
|
+
/**
|
|
3517
|
+
* =============================================================
|
|
3518
|
+
* CACHE
|
|
3519
|
+
* =============================================================
|
|
3520
|
+
*/
|
|
3521
|
+
|
|
3522
|
+
|
|
3523
|
+
/**
|
|
3524
|
+
* Инициализирует кэш стилей для элемента.
|
|
3525
|
+
* @since 1.0.0
|
|
3526
|
+
* @protected
|
|
3527
|
+
* @returns {void}
|
|
3528
|
+
*/
|
|
3529
|
+
_initCacheStyles() {
|
|
3530
|
+
if (!jsse_styles.get(this._element)) {
|
|
3531
|
+
jsse_styles.set(this._element, {});
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
/**
|
|
3536
|
+
* Удаляет кэш стилей элемента.
|
|
3537
|
+
* @since 1.0.0
|
|
3538
|
+
* @protected
|
|
3539
|
+
* @returns {void}
|
|
3540
|
+
*/
|
|
3541
|
+
_deleteCacheStyles() {
|
|
3542
|
+
jsse_styles.delete(this._element);
|
|
3543
|
+
}
|
|
3544
|
+
|
|
3545
|
+
/**
|
|
3546
|
+
* Получает контроллер, связанный с элементом (из глобальной WeakMap).
|
|
3547
|
+
* @since 1.0.0
|
|
3548
|
+
* @protected
|
|
3549
|
+
* @returns {SuperellipseController|undefined}
|
|
3550
|
+
*/
|
|
3551
|
+
_getController() {
|
|
3552
|
+
return jsse_controllers.get(this._element);
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
/**
|
|
3556
|
+
* Проверяет, существует ли контроллер для элемента.
|
|
3557
|
+
* @since 1.0.0
|
|
3558
|
+
* @protected
|
|
3559
|
+
* @returns {boolean}
|
|
3560
|
+
*/
|
|
3561
|
+
_inControllers() {
|
|
3562
|
+
return !!this._getController();
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
/**
|
|
3566
|
+
* Удаляет ссылку на контроллер из глобальной WeakMap.
|
|
3567
|
+
* @since 1.0.0
|
|
3568
|
+
* @protected
|
|
3569
|
+
* @returns {void}
|
|
3570
|
+
*/
|
|
3571
|
+
_deleteFromControllers() {
|
|
3572
|
+
jsse_controllers.delete(this._element);
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
|
|
3576
|
+
/**
|
|
3577
|
+
* =============================================================
|
|
3578
|
+
* STYLESHEET
|
|
3579
|
+
* =============================================================
|
|
3580
|
+
*/
|
|
3581
|
+
|
|
3582
|
+
/**
|
|
3583
|
+
* Инициализирует парсинг стилей и находит триггеры для hover.
|
|
3584
|
+
* @since 1.1.0
|
|
3585
|
+
* @protected
|
|
3586
|
+
* @returns {void}
|
|
3587
|
+
*/
|
|
3588
|
+
_initStylesheet() {
|
|
3589
|
+
this._targetTriggers = this._getTargetTriggers();
|
|
3590
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[INIT]');
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
/**
|
|
3594
|
+
* Уничтожает данные стилей и обработчики hover.
|
|
3595
|
+
* @since 1.1.0
|
|
3596
|
+
* @protected
|
|
3597
|
+
* @returns {void}
|
|
3598
|
+
*/
|
|
3599
|
+
_destroyStylesheet() {
|
|
3600
|
+
this._targetTriggers = null;
|
|
3601
|
+
this._hoverHandlers = null;
|
|
3602
|
+
this._unregisterGlobalTouchEndListener();
|
|
3603
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[DESTROY]');
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
/**
|
|
3607
|
+
* Регистрирует обработчики событий на элементах-триггерах.
|
|
3608
|
+
* @since 1.1.0
|
|
3609
|
+
* @protected
|
|
3610
|
+
* @param {Object} triggers - Объект, где ключ – селектор, значение – массив элементов.
|
|
3611
|
+
* @returns {void}
|
|
3612
|
+
*/
|
|
3613
|
+
_registerTargetListeners(triggers) {
|
|
3614
|
+
this._hoverHandlers = {};
|
|
3615
|
+
for (const selector in triggers) {
|
|
3616
|
+
this._hoverHandlers[selector] = {
|
|
3617
|
+
in : (event) => { this._triggerHandlerIn(selector, event); },
|
|
3618
|
+
out : (event) => { this._triggerHandlerOut(selector, event); },
|
|
3619
|
+
touchStart : (event) => { this._triggerHandlerTouchIn(selector, event); },
|
|
3620
|
+
on : [],
|
|
3621
|
+
hovered : false,
|
|
3622
|
+
touchCount : 0
|
|
3623
|
+
};
|
|
3624
|
+
triggers[selector].forEach((trigger) => {
|
|
3625
|
+
this._hoverHandlers[selector].on.push(trigger);
|
|
3626
|
+
this._registerTriggerListener(trigger, selector);
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3629
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[EVENTS]', true);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
/**
|
|
3633
|
+
* Удаляет все зарегистрированные обработчики с триггеров.
|
|
3634
|
+
* @since 1.1.0
|
|
3635
|
+
* @protected
|
|
3636
|
+
* @returns {void}
|
|
3637
|
+
*/
|
|
3638
|
+
_unregisterTargetListeners() {
|
|
3639
|
+
for (const selector in this._hoverHandlers) {
|
|
3640
|
+
for (const trigger of this._hoverHandlers[selector].on) {
|
|
3641
|
+
if (trigger && trigger.removeEventListener) {
|
|
3642
|
+
this._unregisterTriggerListener(trigger, selector);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
this._hoverHandlers = {};
|
|
3647
|
+
|
|
3648
|
+
// Удаляем глобальный обработчик touchend
|
|
3649
|
+
if (this._globalTouchEndHandler) {
|
|
3650
|
+
document.body.removeEventListener('touchend', this._globalTouchEndHandler);
|
|
3651
|
+
this._globalTouchEndHandler = null;
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TARGET]', '[EVENTS]', false);
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
/**
|
|
3658
|
+
* Добавляет обработчики на конкретный элемент-триггер.
|
|
3659
|
+
* @since 1.1.0
|
|
3660
|
+
* @protected
|
|
3661
|
+
* @param {Element} trigger - DOM-элемент-триггер.
|
|
3662
|
+
* @param {string} selector - Селектор, связанный с триггером.
|
|
3663
|
+
* @returns {void}
|
|
3664
|
+
*/
|
|
3665
|
+
_registerTriggerListener(trigger, selector) {
|
|
3666
|
+
// Мышь / перо
|
|
3667
|
+
trigger.addEventListener('mouseenter', this._hoverHandlers[selector].in);
|
|
3668
|
+
trigger.addEventListener('mouseleave', this._hoverHandlers[selector].out);
|
|
3669
|
+
|
|
3670
|
+
// Касание
|
|
3671
|
+
trigger.addEventListener('touchstart', this._hoverHandlers[selector].touchStart);
|
|
3672
|
+
this._registerGlobalTouchEndListener();
|
|
3673
|
+
|
|
3674
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TRIGGER]', '[EVENT]', true, selector);
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
/**
|
|
3678
|
+
* Регистрирует глобальный обработчик `touchend` для корректной работы hover на сенсорных экранах.
|
|
3679
|
+
* @since 1.1.0
|
|
3680
|
+
* @protected
|
|
3681
|
+
* @returns {void}
|
|
3682
|
+
*/
|
|
3683
|
+
_registerGlobalTouchEndListener() {
|
|
3684
|
+
// Глобальный обработчик touchend (добавляем один раз)
|
|
3685
|
+
if (!this._globalTouchEndHandler) {
|
|
3686
|
+
this._globalTouchEndHandler = (event) => {
|
|
3687
|
+
for (const selector in this._hoverHandlers) {
|
|
3688
|
+
const handler = this._hoverHandlers[selector];
|
|
3689
|
+
if (handler.touchCount > 0) {
|
|
3690
|
+
this._triggerHandlerTouchOut(selector, event);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3694
|
+
document.body.addEventListener('touchend', this._globalTouchEndHandler);
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
/**
|
|
3699
|
+
* Удаляет глобальный обработчик `touchend`.
|
|
3700
|
+
* @since 1.1.0
|
|
3701
|
+
* @protected
|
|
3702
|
+
* @returns {void}
|
|
3703
|
+
*/
|
|
3704
|
+
_unregisterGlobalTouchEndListener() {
|
|
3705
|
+
if (this._globalTouchEndHandler) {
|
|
3706
|
+
document.body.removeEventListener('touchend', this._globalTouchEndHandler);
|
|
3707
|
+
this._globalTouchEndHandler = null;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
/**
|
|
3712
|
+
* Удаляет обработчики с элемента-триггера.
|
|
3713
|
+
* @since 1.1.0
|
|
3714
|
+
* @protected
|
|
3715
|
+
* @param {Element} trigger - DOM-элемент-триггер.
|
|
3716
|
+
* @param {string} selector - Селектор, связанный с триггером.
|
|
3717
|
+
* @returns {void}
|
|
3718
|
+
*/
|
|
3719
|
+
_unregisterTriggerListener(trigger, selector) {
|
|
3720
|
+
trigger.removeEventListener('mouseenter', this._hoverHandlers[selector].in);
|
|
3721
|
+
trigger.removeEventListener('mouseleave', this._hoverHandlers[selector].out);
|
|
3722
|
+
trigger.removeEventListener('touchstart', this._hoverHandlers[selector].touchStart);
|
|
3723
|
+
|
|
3724
|
+
jsse_console.debug({label:'STYLESHEET',element:this._element}, '[TRIGGER]', '[EVENT]', false, selector);
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
/**
|
|
3728
|
+
* Обработчик события `mouseenter` / `pointerenter` на триггере.
|
|
3729
|
+
* @since 1.1.0
|
|
3730
|
+
* @protected
|
|
3731
|
+
* @param {string} selector - Селектор триггера.
|
|
3732
|
+
* @param {Event} event - Событие.
|
|
3733
|
+
* @returns {void}
|
|
3734
|
+
*/
|
|
3735
|
+
_triggerHandlerIn(selector, event) {
|
|
3736
|
+
if ( !this._element.matches(selector) || !this._hoverHandlers[selector] ) return;
|
|
3737
|
+
this._hoverHandlers[selector].hovered = true;
|
|
3738
|
+
jsse_console.debug({label:'HOVER',element:this._element}, '[IN]', selector, event);
|
|
3739
|
+
this._mutationHandler();
|
|
3740
|
+
}
|
|
3741
|
+
|
|
3742
|
+
/**
|
|
3743
|
+
* Обработчик события `mouseleave` / `pointerleave` на триггере.
|
|
3744
|
+
* @since 1.1.0
|
|
3745
|
+
* @protected
|
|
3746
|
+
* @param {string} selector - Селектор триггера.
|
|
3747
|
+
* @param {Event} event - Событие.
|
|
3748
|
+
* @returns {void}
|
|
3749
|
+
*/
|
|
3750
|
+
_triggerHandlerOut(selector, event) {
|
|
3751
|
+
if ( this._element.matches(selector) || !this._hoverHandlers[selector] || !this._hoverHandlers[selector]?.hovered ) return;
|
|
3752
|
+
this._hoverHandlers[selector].hovered = false;
|
|
3753
|
+
jsse_console.debug({label:'HOVER',element:this._element}, '[OUT]', selector, event);
|
|
3754
|
+
this._mutationHandler();
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
/**
|
|
3758
|
+
* Обработчик `touchstart` на триггере (увеличивает счётчик касаний).
|
|
3759
|
+
* @since 1.1.0
|
|
3760
|
+
* @protected
|
|
3761
|
+
* @param {string} selector - Селектор триггера.
|
|
3762
|
+
* @param {TouchEvent} event - Событие касания.
|
|
3763
|
+
* @returns {void}
|
|
3764
|
+
*/
|
|
3765
|
+
_triggerHandlerTouchIn(selector, event) {
|
|
3766
|
+
if ( !this._hoverHandlers[selector] || this._hoverHandlers[selector]?.touchCount !== undefined ) {
|
|
3767
|
+
this._hoverHandlers[selector].touchCount++;
|
|
3768
|
+
if (this._hoverHandlers[selector].touchCount === 1) {
|
|
3769
|
+
this._triggerHandlerIn(selector, event);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
/**
|
|
3775
|
+
* Обработчик `touchend` (уменьшает счётчик касаний и вызывает выход при обнулении).
|
|
3776
|
+
* @since 1.1.0
|
|
3777
|
+
* @protected
|
|
3778
|
+
* @param {string} selector - Селектор триггера.
|
|
3779
|
+
* @param {TouchEvent} event - Событие касания.
|
|
3780
|
+
* @returns {void}
|
|
3781
|
+
*/
|
|
3782
|
+
_triggerHandlerTouchOut(selector, event) {
|
|
3783
|
+
if ( !this._hoverHandlers[selector] || this._hoverHandlers[selector]?.touchCount !== undefined ) {
|
|
3784
|
+
this._hoverHandlers[selector].touchCount--;
|
|
3785
|
+
if (this._hoverHandlers[selector].touchCount === 0) {
|
|
3786
|
+
this._triggerHandlerOut(selector, event);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
/**
|
|
3792
|
+
* Находит все элементы-триггеры, которые могут вызвать изменение стилей при наведении на целевой элемент.
|
|
3793
|
+
* @since 1.1.0
|
|
3794
|
+
* @protected
|
|
3795
|
+
* @returns {Object<string, Element[]>} Объект, где ключ – селектор, значение – массив элементов.
|
|
3796
|
+
*/
|
|
3797
|
+
_getTargetTriggers() {
|
|
3798
|
+
const triggerList = {};
|
|
3799
|
+
const targetSelectors = jsse_stylesheet.getTargetSelectors(this._element, {selectorHasHover:true});
|
|
3800
|
+
for (const targetSelector of targetSelectors) {
|
|
3801
|
+
const selectorTargetElements = this._getSelectorTriggerElements(targetSelector);
|
|
3802
|
+
if (selectorTargetElements.length > 0) {
|
|
3803
|
+
const selector = targetSelector.getSelector();
|
|
3804
|
+
triggerList[selector] = selectorTargetElements;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
return triggerList;
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
/**
|
|
3811
|
+
* Для заданного CSS-правила (селектора) возвращает массив элементов-триггеров.
|
|
3812
|
+
* @since 1.1.0
|
|
3813
|
+
* @protected
|
|
3814
|
+
* @param {StylesheetParserSelector} selector - Объект селектора.
|
|
3815
|
+
* @returns {Element[]}
|
|
3816
|
+
*/
|
|
3817
|
+
_getSelectorTriggerElements(selector) {
|
|
3818
|
+
const selectorTargetElements = [];
|
|
3819
|
+
const selectorParts = selector.getTriggerParts();
|
|
3820
|
+
for (const selectorPart of selectorParts) {
|
|
3821
|
+
const triggerElements = this._getSelectorPartTriggerElements(selectorPart);
|
|
3822
|
+
selectorTargetElements.push(...triggerElements);
|
|
3823
|
+
}
|
|
3824
|
+
return selectorTargetElements;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
/**
|
|
3828
|
+
* Для одной части составного селектора возвращает элементы-триггеры.
|
|
3829
|
+
* @since 1.1.0
|
|
3830
|
+
* @protected
|
|
3831
|
+
* @param {Object} selectorPart - Часть селектора с полями parent, neighbor, child.
|
|
3832
|
+
* @returns {Element[]}
|
|
3833
|
+
*/
|
|
3834
|
+
_getSelectorPartTriggerElements(selectorPart) {
|
|
3835
|
+
if (selectorPart.neighbor) {
|
|
3836
|
+
const neighborSelector = `${selectorPart.neighbor.combinator}${selectorPart.neighbor.clean}`;
|
|
3837
|
+
const cssSelectorHasCombinator = `:has(${selectorPart.neighbor.combinator}*)`;
|
|
3838
|
+
const hasCombinatorIsSupport = jsse_css_selector.isSupport(cssSelectorHasCombinator);
|
|
3839
|
+
if (hasCombinatorIsSupport) {
|
|
3840
|
+
return this._getSelectorPartTriggerElementsWithHasSupport(selectorPart, neighborSelector);
|
|
3841
|
+
} else {
|
|
3842
|
+
// Браузер НЕ поддерживает :has() — используем fallback
|
|
3843
|
+
jsse_console.warn({label:'HOVER', element: this._element}, '[FALLBACK] Using manual DOM traversal for:', neighborSelector);
|
|
3844
|
+
return this._getSelectorPartTriggerElementsWithoutHasSupport(selectorPart.parent, neighborSelector, selectorPart.child);
|
|
3845
|
+
}
|
|
3846
|
+
} else {
|
|
3847
|
+
// Нет соседа — обычный селектор
|
|
3848
|
+
const triggers = Array.from(document.querySelectorAll(selectorPart.parent));
|
|
3849
|
+
return triggers.filter(trigger =>
|
|
3850
|
+
this._elementMatchesChildSelector(trigger, selectorPart.child)
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
/**
|
|
3856
|
+
* Реализация поиска триггеров с использованием современного CSS-селектора `:has()`.
|
|
3857
|
+
* @since 1.1.0
|
|
3858
|
+
* @protected
|
|
3859
|
+
* @param {Object} selectorPart - Часть селектора.
|
|
3860
|
+
* @param {string} neighborSelector - Селектор соседнего элемента.
|
|
3861
|
+
* @returns {Element[]}
|
|
3862
|
+
*/
|
|
3863
|
+
_getSelectorPartTriggerElementsWithHasSupport(selectorPart, neighborSelector) {
|
|
3864
|
+
// Браузер поддерживает :has() — используем быстрый селектор
|
|
3865
|
+
const triggersSelector = `${selectorPart.parent}:has(${neighborSelector})`;
|
|
3866
|
+
const siblingSelector = `${selectorPart.parent}${neighborSelector}`;
|
|
3867
|
+
|
|
3868
|
+
const triggers = Array.from(document.querySelectorAll(triggersSelector));
|
|
3869
|
+
const siblings = Array.from(document.querySelectorAll(siblingSelector));
|
|
3870
|
+
|
|
3871
|
+
return triggers.filter((trigger, index) => {
|
|
3872
|
+
const current = siblings[index];
|
|
3873
|
+
return this._elementMatchesChildSelector(current, selectorPart.child);
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
/**
|
|
3878
|
+
* Fallback-реализация поиска триггеров для браузеров без поддержки `:has()`.
|
|
3879
|
+
* @since 1.1.0
|
|
3880
|
+
* @protected
|
|
3881
|
+
* @param {string} parentSelector - Селектор родителя.
|
|
3882
|
+
* @param {string} neighborSelector - Селектор соседа (с комбинатором).
|
|
3883
|
+
* @param {string} childSelector - Селектор дочернего элемента (целевой элемент).
|
|
3884
|
+
* @returns {Element[]}
|
|
3885
|
+
*/
|
|
3886
|
+
_getSelectorPartTriggerElementsWithoutHasSupport(parentSelector, neighborSelector, childSelector) {
|
|
3887
|
+
const result = [];
|
|
3888
|
+
|
|
3889
|
+
// 1. Находим всех потенциальных родителей
|
|
3890
|
+
const allParents = Array.from(document.querySelectorAll(parentSelector));
|
|
3891
|
+
|
|
3892
|
+
// 2. Парсим комбинатор и чистый селектор соседа
|
|
3893
|
+
const combinator = neighborSelector.trim()[0]; // '+' или '~'
|
|
3894
|
+
const cleanNeighborSelector = neighborSelector.trim().substring(1).trim();
|
|
3895
|
+
|
|
3896
|
+
for (const parent of allParents) {
|
|
3897
|
+
// 3. Ищем соседние элементы относительно родителя или внутри него
|
|
3898
|
+
let neighborElements = [];
|
|
3899
|
+
|
|
3900
|
+
if (combinator === '+') {
|
|
3901
|
+
// Соседний элемент (сразу следующий)
|
|
3902
|
+
const nextSibling = parent.nextElementSibling;
|
|
3903
|
+
if (nextSibling && nextSibling.matches(cleanNeighborSelector)) {
|
|
3904
|
+
neighborElements = [nextSibling];
|
|
3905
|
+
}
|
|
3906
|
+
} else if (combinator === '~') {
|
|
3907
|
+
// Все последующие соседние элементы
|
|
3908
|
+
let sibling = parent.nextElementSibling;
|
|
3909
|
+
while (sibling) {
|
|
3910
|
+
if (sibling.matches(cleanNeighborSelector)) {
|
|
3911
|
+
neighborElements.push(sibling);
|
|
3912
|
+
}
|
|
3913
|
+
sibling = sibling.nextElementSibling;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
// 4. Проверяем, содержит ли найденный сосед целевой элемент
|
|
3918
|
+
for (const neighbor of neighborElements) {
|
|
3919
|
+
if (this._elementMatchesChildSelector(neighbor, childSelector)) {
|
|
3920
|
+
result.push(parent);
|
|
3921
|
+
break; // Нашли триггер для этого родителя
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
return result;
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
/**
|
|
3930
|
+
* Возвращает список элементов, соответствующих селектору, в заданном контексте.
|
|
3931
|
+
* @since 1.1.0
|
|
3932
|
+
* @protected
|
|
3933
|
+
* @param {string} selector - CSS-селектор.
|
|
3934
|
+
* @param {Element|Document} [parent=document] - Корневой элемент для поиска.
|
|
3935
|
+
* @returns {NodeListOf<Element>}
|
|
3936
|
+
*/
|
|
3937
|
+
_getSelectorElements(selector, parent=document) {
|
|
3938
|
+
return parent.querySelectorAll(selector);
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
/**
|
|
3942
|
+
* Проверяет, содержится ли целевой элемент внутри родителя с учётом дочернего селектора.
|
|
3943
|
+
* @since 1.1.0
|
|
3944
|
+
* @protected
|
|
3945
|
+
* @param {Element} parent - Потенциальный родитель.
|
|
3946
|
+
* @param {string} selector - Селектор дочернего элемента.
|
|
3947
|
+
* @returns {boolean}
|
|
3948
|
+
*/
|
|
3949
|
+
_elementMatchesChildSelector(parent, selector) {
|
|
3950
|
+
if (!(parent.contains(this._element) || parent === this._element)) {
|
|
3951
|
+
return false;
|
|
3952
|
+
}
|
|
3953
|
+
if (parent === this._element) return true;
|
|
3954
|
+
|
|
3955
|
+
const children = this._getSelectorElements(selector, parent);
|
|
3956
|
+
return Array.from(children).includes(this._element);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
/**
|
|
3960
|
+
* Заглушка для получения элементов-триггеров (не используется).
|
|
3961
|
+
* @since 1.1.0
|
|
3962
|
+
* @protected
|
|
3963
|
+
* @returns {void}
|
|
3964
|
+
*/
|
|
3965
|
+
_getTriggerElements() {
|
|
3966
|
+
jsse_stylesheet.getTargetSelectors(this._element, {selectorHasHover:true});
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
|
|
3970
|
+
/**
|
|
3971
|
+
* =============================================================
|
|
3972
|
+
* OBSERVERS
|
|
3973
|
+
* =============================================================
|
|
3974
|
+
*/
|
|
3975
|
+
|
|
3976
|
+
/**
|
|
3977
|
+
* Подключает наблюдатели: MutationObserver, ResizeObserver, IntersectionObserver и отслеживание удаления.
|
|
3978
|
+
* @since 1.0.0
|
|
3979
|
+
* @protected
|
|
3980
|
+
* @returns {void}
|
|
3981
|
+
*/
|
|
3982
|
+
_connectObservers() {
|
|
3983
|
+
this._mutationObserver = new MutationObserver(() => {
|
|
3984
|
+
this._mutationHandler();
|
|
3985
|
+
});
|
|
3986
|
+
this._mutationObserver.observe(this._element, {
|
|
3987
|
+
attributes: true,
|
|
3988
|
+
attributeFilter: ['style', 'class']
|
|
3989
|
+
});
|
|
3990
|
+
|
|
3991
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
3992
|
+
this._intersectionObserver = new IntersectionObserver((entries) => {
|
|
3993
|
+
this._intersectionHandler(entries);
|
|
3994
|
+
});
|
|
3995
|
+
this._intersectionObserver.observe(this._element);
|
|
3996
|
+
}
|
|
3997
|
+
|
|
3998
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
3999
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
4000
|
+
this._resizeHandler();
|
|
4001
|
+
});
|
|
4002
|
+
this._resizeObserver.observe(this._element);
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
this._removalObserver = new MutationObserver(() => {
|
|
4006
|
+
this._destroyHandler();
|
|
4007
|
+
});
|
|
4008
|
+
this._removalObserver.observe(document.body, {
|
|
4009
|
+
childList: true,
|
|
4010
|
+
subtree: true
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
/**
|
|
4015
|
+
* Отключает всех наблюдателей и очищает таймеры.
|
|
4016
|
+
* @since 1.0.0
|
|
4017
|
+
* @protected
|
|
4018
|
+
* @returns {void}
|
|
4019
|
+
*/
|
|
4020
|
+
_disconnectObservers() {
|
|
4021
|
+
if (this._prepareTimer) clearTimeout(this._prepareTimer);
|
|
4022
|
+
if (this._executeTimer) clearTimeout(this._executeTimer);
|
|
4023
|
+
|
|
4024
|
+
if (this._resizeObserver) this._resizeObserver.disconnect();
|
|
4025
|
+
if (this._mutationObserver) this._mutationObserver.disconnect();
|
|
4026
|
+
if (this._intersectionObserver) this._intersectionObserver.disconnect();
|
|
4027
|
+
if (this._removalObserver) this._removalObserver.disconnect();
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
/**
|
|
4031
|
+
* Обработчик мутаций (изменение атрибутов style/class). Запускает отложенное обновление.
|
|
4032
|
+
* @since 1.0.0
|
|
4033
|
+
* @protected
|
|
4034
|
+
* @returns {void}
|
|
4035
|
+
*/
|
|
4036
|
+
_mutationHandler() {
|
|
4037
|
+
jsse_console.debug({label:'MUTATION', element:this._element}, '[DETECT]', this._isSelfMutation ? 'self' : 'flow');
|
|
4038
|
+
if (this._isSelfMutation)
|
|
4039
|
+
return;
|
|
4040
|
+
if (this._prepareTimer !== null) {
|
|
4041
|
+
clearTimeout(this._prepareTimer);
|
|
4042
|
+
}
|
|
4043
|
+
this._prepareTimer = setTimeout(() => {
|
|
4044
|
+
this._prepareTimer = null;
|
|
4045
|
+
jsse_console.debug({label:'MUTATION', element:this._element}, '[START]');
|
|
4046
|
+
this._isSelfMutation = true;
|
|
4047
|
+
try {
|
|
4048
|
+
jsse_console.debug({label:'MUTATION', element:this._element}, '[UPDATE]');
|
|
4049
|
+
if (this._isDisplay() && this._needsUpdate) {
|
|
4050
|
+
this._mode.update();
|
|
4051
|
+
this._emit('update', { type: 'full' });
|
|
4052
|
+
this._needsUpdate = false;
|
|
4053
|
+
} else {
|
|
4054
|
+
this._mode.updateStyles();
|
|
4055
|
+
this._emit('update', { type: 'styles' });
|
|
4056
|
+
}
|
|
4057
|
+
} finally {
|
|
4058
|
+
if (this._executeTimer !== null) {
|
|
4059
|
+
clearTimeout(this._executeTimer);
|
|
4060
|
+
}
|
|
4061
|
+
this._executeTimer = setTimeout(() => {
|
|
4062
|
+
this._executeTimer = null;
|
|
4063
|
+
jsse_console.debug({label:'MUTATION', element:this._element}, '[END]');
|
|
4064
|
+
this._isSelfMutation = false;
|
|
4065
|
+
|
|
4066
|
+
}, 0);
|
|
4067
|
+
}
|
|
4068
|
+
}, 0);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
/**
|
|
4072
|
+
* Обработчик изменения размеров элемента. При скрытом элементе устанавливает флаг `_needsUpdate`.
|
|
4073
|
+
* @since 1.0.0
|
|
4074
|
+
* @protected
|
|
4075
|
+
* @returns {void}
|
|
4076
|
+
*/
|
|
4077
|
+
_resizeHandler() {
|
|
4078
|
+
if (this._isDisplay()) {
|
|
4079
|
+
try {
|
|
4080
|
+
this._mode.updateSize();
|
|
4081
|
+
this._emit('update', { type: 'size' });
|
|
4082
|
+
} finally {
|
|
4083
|
+
}
|
|
4084
|
+
} else {
|
|
4085
|
+
this._needsUpdate = true;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
/**
|
|
4090
|
+
* Обработчик видимости элемента (IntersectionObserver). При появлении элемента выполняет отложенное обновление.
|
|
4091
|
+
* @since 1.0.0
|
|
4092
|
+
* @protected
|
|
4093
|
+
* @param {IntersectionObserverEntry[]} entries - Записи пересечений.
|
|
4094
|
+
* @returns {void}
|
|
4095
|
+
*/
|
|
4096
|
+
_intersectionHandler(entries) {
|
|
4097
|
+
if (entries[0].isIntersecting && this._needsUpdate) {
|
|
4098
|
+
try {
|
|
4099
|
+
this._mode.update();
|
|
4100
|
+
this._emit('update', { type: 'full' });
|
|
4101
|
+
this._needsUpdate = false;
|
|
4102
|
+
} finally {
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
/**
|
|
4108
|
+
* Обработчик удаления элемента из DOM. При отсутствии элемента в документе уничтожает контроллер.
|
|
4109
|
+
* @since 1.0.0
|
|
4110
|
+
* @protected
|
|
4111
|
+
* @returns {void}
|
|
4112
|
+
*/
|
|
4113
|
+
_destroyHandler() {
|
|
4114
|
+
if (!document.body.contains(this._element)) {
|
|
4115
|
+
this._destroyController();
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
/**
|
|
4121
|
+
* @file src/api.js
|
|
4122
|
+
*
|
|
4123
|
+
* @module sj-superellipse/api
|
|
4124
|
+
* @since 1.0.0
|
|
4125
|
+
* @author f4n70m
|
|
4126
|
+
*
|
|
4127
|
+
* @description
|
|
4128
|
+
* Публичное API библиотеки. Определяет глобальную функцию инициализации `superellipseInit`,
|
|
4129
|
+
* а также расширяет `Element.prototype` методами `superellipse` (геттер) и `superellipseInit`.
|
|
4130
|
+
* Управляет слабой картой контроллеров `jsse_controllers`.
|
|
4131
|
+
*
|
|
4132
|
+
* @example
|
|
4133
|
+
* // Инициализация через глобальную функцию
|
|
4134
|
+
* const controller = superellipseInit('.card', { curveFactor: 1.2 });
|
|
4135
|
+
*
|
|
4136
|
+
* @example
|
|
4137
|
+
* // Инициализация через метод элемента
|
|
4138
|
+
* const el = document.querySelector('.card');
|
|
4139
|
+
* el.superellipseInit({ mode: 'clip-path' });
|
|
4140
|
+
* const controller = el.superellipse;
|
|
4141
|
+
*/
|
|
4142
|
+
|
|
4143
|
+
|
|
4144
|
+
/**
|
|
4145
|
+
* Геттер для доступа к контроллеру через element.superellipse.
|
|
4146
|
+
* @name Element.prototype.superellipse
|
|
4147
|
+
* @function
|
|
4148
|
+
* @returns {SuperellipseController|undefined} Контроллер, если элемент инициализирован, иначе undefined.
|
|
4149
|
+
*/
|
|
4150
|
+
Object.defineProperty(Element.prototype, 'superellipse', {
|
|
4151
|
+
get() {
|
|
4152
|
+
return jsse_controllers.get(this);
|
|
4153
|
+
}
|
|
4154
|
+
});
|
|
4155
|
+
|
|
4156
|
+
|
|
4157
|
+
/**
|
|
4158
|
+
* Инициализирует контроллер суперэллипса на DOM-элементе.
|
|
4159
|
+
* @name Element.prototype.superellipseInit
|
|
4160
|
+
* @function
|
|
4161
|
+
* @param {Object} [options] - Опции инициализации.
|
|
4162
|
+
* @param {boolean} [options.force] - Если true, пересоздаёт контроллер, даже если он уже существует.
|
|
4163
|
+
* @param {string} [options.mode='svg-layer'] - Режим работы ('svg-layer' или 'clip-path').
|
|
4164
|
+
* @param {number} [options.curveFactor] - Коэффициент кривизны углов (диапазон -2..2).
|
|
4165
|
+
* @param {number} [options.precision=2] - Количество знаков после запятой в генерируемом пути.
|
|
4166
|
+
* @returns {SuperellipseController} Контроллер, связанный с элементом.
|
|
4167
|
+
*/
|
|
4168
|
+
Element.prototype.superellipseInit = function(options) {
|
|
4169
|
+
let controller = jsse_controllers.get(this);
|
|
4170
|
+
|
|
4171
|
+
if (controller && !options?.force) {
|
|
4172
|
+
jsse_console.warn({label:'API', element: this}, 'The element already has a controller. Use {force:true} to recreate it.');
|
|
4173
|
+
return controller;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
if (controller) {
|
|
4177
|
+
controller.destroy();
|
|
4178
|
+
}
|
|
4179
|
+
controller = new SuperellipseController(this, options);
|
|
4180
|
+
jsse_controllers.set(this, controller);
|
|
4181
|
+
return controller; // для цепочек
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
|
|
4185
|
+
/**
|
|
4186
|
+
* Инициализирует один или несколько элементов суперэллипсом.
|
|
4187
|
+
* @function superellipseInit
|
|
4188
|
+
* @param {string|Element|NodeList|Array<Element>} target - CSS-селектор, DOM-элемент или коллекция элементов.
|
|
4189
|
+
* @param {Object} [options] - Опции инициализации (см. Element.prototype.superellipseInit).
|
|
4190
|
+
* @returns {SuperellipseController|Array<SuperellipseController>} Контроллер для одного элемента или массив контроллеров для нескольких.
|
|
4191
|
+
* @throws {Error} Если первый аргумент не является селектором, элементом или коллекцией.
|
|
4192
|
+
*/
|
|
4193
|
+
function superellipseInit(target, options) {
|
|
4194
|
+
if (typeof target === 'string') {
|
|
4195
|
+
const elements = document.querySelectorAll(target);
|
|
4196
|
+
const controllersList = [];
|
|
4197
|
+
elements.forEach(el => {
|
|
4198
|
+
el.superellipseInit(options);
|
|
4199
|
+
controllersList.push(el.superellipse);
|
|
4200
|
+
});
|
|
4201
|
+
return controllersList;
|
|
4202
|
+
} else if (target instanceof Element) {
|
|
4203
|
+
target.superellipseInit(options);
|
|
4204
|
+
return target.superellipse;
|
|
4205
|
+
} else if (target instanceof NodeList || Array.isArray(target)) {
|
|
4206
|
+
const controllersList = [];
|
|
4207
|
+
for (let i = 0; i < target.length; i++) {
|
|
4208
|
+
const el = target[i];
|
|
4209
|
+
if (el instanceof Element) {
|
|
4210
|
+
el.superellipseInit(options);
|
|
4211
|
+
controllersList.push(el.superellipse);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
return controllersList;
|
|
4215
|
+
} else {
|
|
4216
|
+
throw new Error('superellipseInit: первый аргумент должен быть селектором, элементом или коллекцией элементов');
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
if (typeof window !== 'undefined') {
|
|
4220
|
+
window.superellipseInit = superellipseInit;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
exports.SuperellipseController = SuperellipseController;
|
|
4224
|
+
exports.jsse_generateSuperellipsePath = jsse_generateSuperellipsePath;
|
|
4225
|
+
exports.superellipseInit = superellipseInit;
|
|
4226
|
+
|
|
4227
|
+
}));
|
|
4228
|
+
//# sourceMappingURL=superellipse.js.map
|