@sequent-org/moodboard 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +6 -1
- package/src/initNoBundler.js +147 -0
- package/src/moodboard/MoodBoard.js +4 -1
- package/src/tools/object-tools/PlacementTool.js +53 -18
- package/src/ui/Toolbar.js +145 -24
- package/src/utils/emojiLoaderNoBundler.js +120 -0
- package/src/utils/emojiResolver.js +53 -6
- package/src/utils/styleLoader.js +84 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Основной экспорт пакета - готовый MoodBoard с UI
|
|
2
2
|
export { MoodBoard } from './moodboard/MoodBoard.js';
|
|
3
|
+
|
|
4
|
+
// Дополнительные экспорты для работы без bundler
|
|
5
|
+
export { initMoodBoardNoBundler, quickInitMoodBoard, injectCriticalStyles } from './initNoBundler.js';
|
|
6
|
+
export { StyleLoader } from './utils/styleLoader.js';
|
|
7
|
+
export { EmojiLoaderNoBundler } from './utils/emojiLoaderNoBundler.js';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Инициализация MoodBoard для использования без bundler (в Laravel и др.)
|
|
3
|
+
*/
|
|
4
|
+
import { MoodBoard } from './moodboard/MoodBoard.js';
|
|
5
|
+
import { StyleLoader } from './utils/styleLoader.js';
|
|
6
|
+
import { EmojiLoaderNoBundler } from './utils/emojiLoaderNoBundler.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Инициализирует MoodBoard с автоматической загрузкой стилей и ресурсов
|
|
10
|
+
* @param {string|HTMLElement} container - контейнер для MoodBoard
|
|
11
|
+
* @param {Object} options - опции MoodBoard
|
|
12
|
+
* @param {string} basePath - базовый путь к пакету (например: '/node_modules/moodboard-futurello/')
|
|
13
|
+
* @returns {Promise<MoodBoard>} готовый экземпляр MoodBoard
|
|
14
|
+
*/
|
|
15
|
+
export async function initMoodBoardNoBundler(container, options = {}, basePath = '') {
|
|
16
|
+
console.log('🚀 Инициализация MoodBoard без bundler...');
|
|
17
|
+
|
|
18
|
+
// 1. Загружаем стили
|
|
19
|
+
const styleLoader = new StyleLoader();
|
|
20
|
+
await styleLoader.loadAllStyles(basePath);
|
|
21
|
+
|
|
22
|
+
// 2. Инициализируем загрузчик эмоджи
|
|
23
|
+
const emojiLoader = new EmojiLoaderNoBundler();
|
|
24
|
+
emojiLoader.init(basePath);
|
|
25
|
+
|
|
26
|
+
// 3. Загружаем эмоджи для использования в панели
|
|
27
|
+
const emojiGroups = await emojiLoader.loadEmojis();
|
|
28
|
+
|
|
29
|
+
// 4. Передаем загрузчик эмоджи в опции
|
|
30
|
+
const enhancedOptions = {
|
|
31
|
+
...options,
|
|
32
|
+
emojiLoader: emojiLoader,
|
|
33
|
+
emojiGroups: emojiGroups,
|
|
34
|
+
emojiBasePath: basePath ? `${basePath}src/assets/emodji/` : null,
|
|
35
|
+
noBundler: true
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 5. Создаем MoodBoard
|
|
39
|
+
const moodboard = new MoodBoard(container, enhancedOptions);
|
|
40
|
+
|
|
41
|
+
console.log('✅ MoodBoard инициализирован без bundler');
|
|
42
|
+
|
|
43
|
+
return moodboard;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Инжектирует критичные стили inline для мгновенного отображения
|
|
48
|
+
*/
|
|
49
|
+
export function injectCriticalStyles() {
|
|
50
|
+
const criticalCSS = `
|
|
51
|
+
/* Критичные стили для мгновенного отображения */
|
|
52
|
+
.moodboard-workspace {
|
|
53
|
+
position: relative;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
background: #f7fbff;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.moodboard-toolbar {
|
|
61
|
+
position: absolute;
|
|
62
|
+
left: 20px;
|
|
63
|
+
top: 50%;
|
|
64
|
+
transform: translateY(-50%);
|
|
65
|
+
z-index: 1000;
|
|
66
|
+
background: white;
|
|
67
|
+
border-radius: 12px;
|
|
68
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
69
|
+
padding: 8px;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
gap: 4px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.moodboard-topbar {
|
|
76
|
+
position: absolute;
|
|
77
|
+
top: 20px;
|
|
78
|
+
left: 20px;
|
|
79
|
+
right: 20px;
|
|
80
|
+
z-index: 1000;
|
|
81
|
+
background: white;
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
84
|
+
padding: 8px 16px;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
height: 44px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.moodboard-html-handles {
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: 0;
|
|
94
|
+
left: 0;
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
z-index: 999;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Скрыть до полной загрузки стилей */
|
|
100
|
+
.moodboard-toolbar__popup,
|
|
101
|
+
.moodboard-properties-panel {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
transition: opacity 0.3s ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.moodboard-styles-loaded .moodboard-toolbar__popup,
|
|
107
|
+
.moodboard-styles-loaded .moodboard-properties-panel {
|
|
108
|
+
opacity: 1;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const style = document.createElement('style');
|
|
113
|
+
style.id = 'moodboard-critical-styles';
|
|
114
|
+
style.textContent = criticalCSS;
|
|
115
|
+
document.head.appendChild(style);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Простая инициализация только с критичными стилями
|
|
120
|
+
* Для быстрого запуска, полные стили загружаются асинхронно
|
|
121
|
+
*/
|
|
122
|
+
export function quickInitMoodBoard(container, options = {}, basePath = '') {
|
|
123
|
+
// Инжектируем критичные стили сразу
|
|
124
|
+
injectCriticalStyles();
|
|
125
|
+
|
|
126
|
+
// Загружаем полные стили асинхронно
|
|
127
|
+
const styleLoader = new StyleLoader();
|
|
128
|
+
styleLoader.loadAllStyles(basePath).then(() => {
|
|
129
|
+
// Добавляем класс для показа полностью загруженных стилей
|
|
130
|
+
if (typeof container === 'string') {
|
|
131
|
+
container = document.querySelector(container);
|
|
132
|
+
}
|
|
133
|
+
if (container) {
|
|
134
|
+
container.classList.add('moodboard-styles-loaded');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Создаем MoodBoard с fallback эмоджи
|
|
139
|
+
const moodboard = new MoodBoard(container, {
|
|
140
|
+
...options,
|
|
141
|
+
noBundler: true,
|
|
142
|
+
skipEmojiLoader: true, // Пропускаем автозагрузку эмоджи
|
|
143
|
+
emojiBasePath: basePath ? `${basePath}src/assets/emodji/` : null
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return moodboard;
|
|
147
|
+
}
|
|
@@ -159,7 +159,10 @@ export class MoodBoard {
|
|
|
159
159
|
this.toolbar = new Toolbar(
|
|
160
160
|
this.toolbarContainer,
|
|
161
161
|
this.coreMoodboard.eventBus,
|
|
162
|
-
this.options.theme
|
|
162
|
+
this.options.theme,
|
|
163
|
+
{
|
|
164
|
+
emojiBasePath: this.options.emojiBasePath || null
|
|
165
|
+
}
|
|
163
166
|
);
|
|
164
167
|
|
|
165
168
|
// Добавляем функцию для отладки иконок в window
|
|
@@ -103,9 +103,16 @@ export class PlacementTool extends BaseTool {
|
|
|
103
103
|
|
|
104
104
|
// Обработка выбора файла
|
|
105
105
|
this.eventBus.on(Events.Place.FileSelected, (fileData) => {
|
|
106
|
+
console.log('📁 PlacementTool: получен FileSelected:', fileData);
|
|
106
107
|
this.selectedFile = fileData;
|
|
107
108
|
this.selectedImage = null;
|
|
108
|
-
|
|
109
|
+
|
|
110
|
+
// Если PlacementTool уже активен - показываем призрак сразу
|
|
111
|
+
if (this.world) {
|
|
112
|
+
this.showFileGhost();
|
|
113
|
+
} else {
|
|
114
|
+
console.log('📁 PlacementTool: world не готов, призрак будет показан при активации');
|
|
115
|
+
}
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
// Обработка отмены выбора файла
|
|
@@ -547,7 +554,16 @@ export class PlacementTool extends BaseTool {
|
|
|
547
554
|
* Показать "призрак" файла
|
|
548
555
|
*/
|
|
549
556
|
showFileGhost() {
|
|
550
|
-
|
|
557
|
+
console.log('📁 PlacementTool.showFileGhost:', {
|
|
558
|
+
hasSelectedFile: !!this.selectedFile,
|
|
559
|
+
hasWorld: !!this.world,
|
|
560
|
+
selectedFileData: this.selectedFile
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
if (!this.selectedFile || !this.world) {
|
|
564
|
+
console.warn('⚠️ Не можем показать призрак файла - нет selectedFile или world');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
551
567
|
|
|
552
568
|
this.hideGhost(); // Сначала убираем старый призрак
|
|
553
569
|
|
|
@@ -562,9 +578,10 @@ export class PlacementTool extends BaseTool {
|
|
|
562
578
|
const worldPoint = this._toWorld(cursorX, cursorY);
|
|
563
579
|
this.updateGhostPosition(worldPoint.x, worldPoint.y);
|
|
564
580
|
}
|
|
565
|
-
// Попробуем дождаться загрузки веб-шрифта Caveat до отрисовки
|
|
566
|
-
|
|
567
|
-
const
|
|
581
|
+
// Попробуем дождаться загрузки веб-шрифта Caveat до отрисовки
|
|
582
|
+
// Для файлов используем selectedFile, а не pending
|
|
583
|
+
const fileFont = (this.selectedFile.properties?.fontFamily) || 'Caveat, Arial, cursive';
|
|
584
|
+
const primaryFont = String(fileFont).split(',')[0].trim().replace(/^['"]|['"]$/g, '') || 'Caveat';
|
|
568
585
|
|
|
569
586
|
// Размеры
|
|
570
587
|
const width = this.selectedFile.properties.width || 120;
|
|
@@ -622,6 +639,13 @@ export class PlacementTool extends BaseTool {
|
|
|
622
639
|
this.ghostContainer.pivot.y = height / 2;
|
|
623
640
|
|
|
624
641
|
this.world.addChild(this.ghostContainer);
|
|
642
|
+
|
|
643
|
+
console.log('📁 Призрак файла создан и добавлен в world:', {
|
|
644
|
+
ghostContainerSize: { w: width, h: height },
|
|
645
|
+
ghostContainerAlpha: this.ghostContainer.alpha,
|
|
646
|
+
worldHasContainer: this.world.children.includes(this.ghostContainer),
|
|
647
|
+
ghostContainerChildren: this.ghostContainer.children.length
|
|
648
|
+
});
|
|
625
649
|
}
|
|
626
650
|
|
|
627
651
|
/**
|
|
@@ -722,9 +746,10 @@ export class PlacementTool extends BaseTool {
|
|
|
722
746
|
this.ghostContainer = new PIXI.Container();
|
|
723
747
|
this.ghostContainer.alpha = 0.6; // Полупрозрачность
|
|
724
748
|
|
|
725
|
-
// Размеры призрака
|
|
726
|
-
const
|
|
727
|
-
const
|
|
749
|
+
// Размеры призрака - используем размеры из pending/selected, если есть
|
|
750
|
+
const isEmojiIcon = this.selectedImage.properties?.isEmojiIcon;
|
|
751
|
+
const maxWidth = this.selectedImage.properties.width || (isEmojiIcon ? 64 : 300);
|
|
752
|
+
const maxHeight = this.selectedImage.properties.height || (isEmojiIcon ? 64 : 200);
|
|
728
753
|
|
|
729
754
|
try {
|
|
730
755
|
// Создаем превью изображения
|
|
@@ -816,8 +841,10 @@ export class PlacementTool extends BaseTool {
|
|
|
816
841
|
this.ghostContainer = new PIXI.Container();
|
|
817
842
|
this.ghostContainer.alpha = 0.6;
|
|
818
843
|
|
|
819
|
-
|
|
820
|
-
const
|
|
844
|
+
// Для эмоджи используем точные размеры из pending для согласованности
|
|
845
|
+
const isEmojiIcon = this.pending.properties?.isEmojiIcon;
|
|
846
|
+
const maxWidth = this.pending.size?.width || this.pending.properties?.width || (isEmojiIcon ? 64 : 56);
|
|
847
|
+
const maxHeight = this.pending.size?.height || this.pending.properties?.height || (isEmojiIcon ? 64 : 56);
|
|
821
848
|
|
|
822
849
|
try {
|
|
823
850
|
const texture = await PIXI.Texture.fromURL(src);
|
|
@@ -853,15 +880,23 @@ export class PlacementTool extends BaseTool {
|
|
|
853
880
|
|
|
854
881
|
this.world.addChild(this.ghostContainer);
|
|
855
882
|
|
|
856
|
-
//
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
883
|
+
// Для эмоджи не используем кастомный курсор, чтобы избежать дублирования призраков
|
|
884
|
+
if (!isEmojiIcon) {
|
|
885
|
+
// Кастомный курсор только для обычных изображений
|
|
886
|
+
try {
|
|
887
|
+
if (this.app && this.app.view && src) {
|
|
888
|
+
const cursorSize = 24;
|
|
889
|
+
const url = encodeURI(src);
|
|
890
|
+
// Используем CSS cursor с изображением, если поддерживается
|
|
891
|
+
this.app.view.style.cursor = `url(${url}) ${Math.floor(cursorSize/2)} ${Math.floor(cursorSize/2)}, default`;
|
|
892
|
+
}
|
|
893
|
+
} catch (_) {}
|
|
894
|
+
} else {
|
|
895
|
+
// Для эмоджи используем стандартный курсор
|
|
896
|
+
if (this.app && this.app.view) {
|
|
897
|
+
this.app.view.style.cursor = 'crosshair';
|
|
863
898
|
}
|
|
864
|
-
}
|
|
899
|
+
}
|
|
865
900
|
}
|
|
866
901
|
|
|
867
902
|
/**
|
package/src/ui/Toolbar.js
CHANGED
|
@@ -5,11 +5,14 @@ import { Events } from '../core/events/Events.js';
|
|
|
5
5
|
import { IconLoader } from '../utils/iconLoader.js';
|
|
6
6
|
|
|
7
7
|
export class Toolbar {
|
|
8
|
-
constructor(container, eventBus, theme = 'light') {
|
|
8
|
+
constructor(container, eventBus, theme = 'light', options = {}) {
|
|
9
9
|
this.container = container;
|
|
10
10
|
this.eventBus = eventBus;
|
|
11
11
|
this.theme = theme;
|
|
12
12
|
|
|
13
|
+
// Базовый путь для ассетов (эмоджи и другие ресурсы)
|
|
14
|
+
this.emojiBasePath = options.emojiBasePath || null;
|
|
15
|
+
|
|
13
16
|
// Инициализируем IconLoader
|
|
14
17
|
this.iconLoader = new IconLoader();
|
|
15
18
|
|
|
@@ -906,24 +909,33 @@ export class Toolbar {
|
|
|
906
909
|
this.emojiPopupEl = document.createElement('div');
|
|
907
910
|
this.emojiPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--emoji';
|
|
908
911
|
this.emojiPopupEl.style.display = 'none';
|
|
909
|
-
// Загружаем все изображения из папки src/assets/emodji (png/svg) через Vite import.meta.glob
|
|
910
|
-
const modules = import.meta.glob('../assets/emodji/**/*.{png,PNG,svg,SVG}', { eager: true, as: 'url' });
|
|
911
912
|
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
913
|
+
// Определяем способ загрузки эмоджи
|
|
914
|
+
let groups = new Map();
|
|
915
|
+
|
|
916
|
+
if (typeof import.meta !== 'undefined' && import.meta.glob) {
|
|
917
|
+
// Режим с bundler (Vite) - используем import.meta.glob
|
|
918
|
+
const modules = import.meta.glob('../assets/emodji/**/*.{png,PNG,svg,SVG}', { eager: true, as: 'url' });
|
|
919
|
+
|
|
920
|
+
// Группируем по подпапкам внутри emodji (категории)
|
|
921
|
+
const entries = Object.entries(modules).sort(([a], [b]) => a.localeCompare(b));
|
|
922
|
+
entries.forEach(([path, url]) => {
|
|
923
|
+
const marker = '/emodji/';
|
|
924
|
+
const idx = path.indexOf(marker);
|
|
925
|
+
let category = 'Разное';
|
|
926
|
+
if (idx >= 0) {
|
|
927
|
+
const after = path.slice(idx + marker.length);
|
|
928
|
+
const parts = after.split('/');
|
|
929
|
+
category = parts.length > 1 ? parts[0] : 'Разное';
|
|
930
|
+
}
|
|
931
|
+
if (!groups.has(category)) groups.set(category, []);
|
|
932
|
+
groups.get(category).push({ path, url });
|
|
933
|
+
});
|
|
934
|
+
} else {
|
|
935
|
+
// Режим без bundler - используем статичный список
|
|
936
|
+
console.log('🎭 Toolbar: Режим без bundler, используем статичные эмоджи');
|
|
937
|
+
groups = this.getFallbackEmojiGroups();
|
|
938
|
+
}
|
|
927
939
|
|
|
928
940
|
// Задаем желаемый порядок категорий
|
|
929
941
|
const ORDER = ['Смайлики', 'Жесты', 'Женские эмоции', 'Котики', 'Разное'];
|
|
@@ -957,9 +969,13 @@ export class Toolbar {
|
|
|
957
969
|
|
|
958
970
|
// Перетаскивание: начинаем только если был реальный drag (движение > 4px)
|
|
959
971
|
btn.addEventListener('mousedown', (e) => {
|
|
972
|
+
// Блокируем одновременную обработку
|
|
973
|
+
if (btn.__clickProcessing || btn.__dragActive) return;
|
|
974
|
+
|
|
960
975
|
const startX = e.clientX;
|
|
961
976
|
const startY = e.clientY;
|
|
962
977
|
let startedDrag = false;
|
|
978
|
+
|
|
963
979
|
const onMove = (ev) => {
|
|
964
980
|
if (startedDrag) return;
|
|
965
981
|
const dx = Math.abs(ev.clientX - startX);
|
|
@@ -967,6 +983,10 @@ export class Toolbar {
|
|
|
967
983
|
if (dx > 4 || dy > 4) {
|
|
968
984
|
startedDrag = true;
|
|
969
985
|
btn.__dragActive = true;
|
|
986
|
+
|
|
987
|
+
// Блокируем click handler
|
|
988
|
+
btn.__clickProcessing = true;
|
|
989
|
+
|
|
970
990
|
const target = 64;
|
|
971
991
|
const targetW = target;
|
|
972
992
|
const targetH = target;
|
|
@@ -985,8 +1005,11 @@ export class Toolbar {
|
|
|
985
1005
|
};
|
|
986
1006
|
const onUp = () => {
|
|
987
1007
|
cleanup();
|
|
988
|
-
//
|
|
989
|
-
setTimeout(() => {
|
|
1008
|
+
// Снимаем флаги с задержкой
|
|
1009
|
+
setTimeout(() => {
|
|
1010
|
+
btn.__dragActive = false;
|
|
1011
|
+
btn.__clickProcessing = false;
|
|
1012
|
+
}, 50);
|
|
990
1013
|
};
|
|
991
1014
|
const cleanup = () => {
|
|
992
1015
|
document.removeEventListener('mousemove', onMove);
|
|
@@ -996,8 +1019,13 @@ export class Toolbar {
|
|
|
996
1019
|
document.addEventListener('mouseup', onUp, { once: true });
|
|
997
1020
|
});
|
|
998
1021
|
|
|
999
|
-
btn.addEventListener('click', () => {
|
|
1000
|
-
|
|
1022
|
+
btn.addEventListener('click', (e) => {
|
|
1023
|
+
// Блокируем обработку клика если был drag или если уже обрабатывается
|
|
1024
|
+
if (btn.__dragActive || btn.__clickProcessing) return;
|
|
1025
|
+
|
|
1026
|
+
btn.__clickProcessing = true;
|
|
1027
|
+
setTimeout(() => { btn.__clickProcessing = false; }, 100);
|
|
1028
|
+
|
|
1001
1029
|
this.animateButton(btn);
|
|
1002
1030
|
const target = 64; // кратно 128 для лучшей четкости при даунскейле
|
|
1003
1031
|
const targetW = target;
|
|
@@ -1019,6 +1047,96 @@ export class Toolbar {
|
|
|
1019
1047
|
this.container.appendChild(this.emojiPopupEl);
|
|
1020
1048
|
}
|
|
1021
1049
|
|
|
1050
|
+
/**
|
|
1051
|
+
* Возвращает fallback группы эмоджи для работы без bundler
|
|
1052
|
+
*/
|
|
1053
|
+
getFallbackEmojiGroups() {
|
|
1054
|
+
const groups = new Map();
|
|
1055
|
+
|
|
1056
|
+
// Определяем базовый путь для эмоджи
|
|
1057
|
+
const basePath = this.getEmojiBasePath();
|
|
1058
|
+
|
|
1059
|
+
// Статичный список эмоджи с реальными именами файлов
|
|
1060
|
+
const fallbackEmojis = {
|
|
1061
|
+
'Смайлики': [
|
|
1062
|
+
'1f600.png', '1f601.png', '1f602.png', '1f603.png', '1f604.png',
|
|
1063
|
+
'1f605.png', '1f606.png', '1f607.png', '1f609.png', '1f60a.png',
|
|
1064
|
+
'1f60b.png', '1f60c.png', '1f60d.png', '1f60e.png', '1f60f.png',
|
|
1065
|
+
'1f610.png', '1f611.png', '1f612.png', '1f613.png', '1f614.png',
|
|
1066
|
+
'1f615.png', '1f616.png', '1f617.png', '1f618.png', '1f619.png'
|
|
1067
|
+
],
|
|
1068
|
+
'Жесты': [
|
|
1069
|
+
'1f446.png', '1f447.png', '1f448.png', '1f449.png', '1f44a.png',
|
|
1070
|
+
'1f44b.png', '1f44c.png', '1f450.png', '1f4aa.png', '1f590.png',
|
|
1071
|
+
'1f596.png', '1f64c.png', '1f64f.png', '270c.png', '270d.png'
|
|
1072
|
+
],
|
|
1073
|
+
'Женские эмоции': [
|
|
1074
|
+
'1f645.png', '1f646.png', '1f64b.png', '1f64d.png', '1f64e.png'
|
|
1075
|
+
],
|
|
1076
|
+
'Котики': [
|
|
1077
|
+
'1f638.png', '1f639.png', '1f63a.png', '1f63b.png', '1f63c.png',
|
|
1078
|
+
'1f63d.png', '1f63e.png', '1f63f.png', '1f640.png'
|
|
1079
|
+
],
|
|
1080
|
+
'Разное': [
|
|
1081
|
+
'1f440.png', '1f441.png', '1f499.png', '1f4a1.png', '1f4a3.png',
|
|
1082
|
+
'1f4a9.png', '1f4ac.png', '1f4af.png', '2764.png', '203c.png', '26d4.png'
|
|
1083
|
+
]
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
Object.entries(fallbackEmojis).forEach(([category, emojis]) => {
|
|
1087
|
+
const emojiList = emojis.map(file => ({
|
|
1088
|
+
path: `${basePath}${category}/${file}`,
|
|
1089
|
+
url: `${basePath}${category}/${file}`
|
|
1090
|
+
}));
|
|
1091
|
+
groups.set(category, emojiList);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
return groups;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Определяет базовый путь для эмоджи в зависимости от режима
|
|
1099
|
+
*/
|
|
1100
|
+
getEmojiBasePath() {
|
|
1101
|
+
// 1. Приоритет: опция basePath из конструктора
|
|
1102
|
+
if (this.emojiBasePath) {
|
|
1103
|
+
return this.emojiBasePath.endsWith('/') ? this.emojiBasePath : this.emojiBasePath + '/';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 2. Глобальная настройка (абсолютный URL)
|
|
1107
|
+
if (window.MOODBOARD_BASE_PATH) {
|
|
1108
|
+
const basePath = window.MOODBOARD_BASE_PATH.endsWith('/') ? window.MOODBOARD_BASE_PATH : window.MOODBOARD_BASE_PATH + '/';
|
|
1109
|
+
return `${basePath}src/assets/emodji/`;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// 3. Вычисление от URL текущего модуля (import.meta.url)
|
|
1113
|
+
try {
|
|
1114
|
+
// Используем import.meta.url для получения абсолютного пути к ассетам
|
|
1115
|
+
const currentModuleUrl = import.meta.url;
|
|
1116
|
+
// От текущего модуля (ui/Toolbar.js) поднимаемся к корню пакета и идем к assets
|
|
1117
|
+
const emojiUrl = new URL('../assets/emodji/', currentModuleUrl).href;
|
|
1118
|
+
return emojiUrl;
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
console.warn('⚠️ Не удалось определить путь через import.meta.url:', error);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// 4. Fallback: поиск script тега для определения базового URL
|
|
1124
|
+
try {
|
|
1125
|
+
const currentScript = document.currentScript;
|
|
1126
|
+
if (currentScript && currentScript.src) {
|
|
1127
|
+
// Пытаемся определить от текущего скрипта
|
|
1128
|
+
const scriptUrl = new URL(currentScript.src);
|
|
1129
|
+
const baseUrl = new URL('../assets/emodji/', scriptUrl).href;
|
|
1130
|
+
return baseUrl;
|
|
1131
|
+
}
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
console.warn('⚠️ Не удалось определить путь через currentScript:', error);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// 5. Последний fallback: абсолютный путь от корня домена
|
|
1137
|
+
return '/src/assets/emodji/';
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1022
1140
|
toggleEmojiPopup(anchorButton) {
|
|
1023
1141
|
if (!this.emojiPopupEl) return;
|
|
1024
1142
|
if (this.emojiPopupEl.style.display === 'none') {
|
|
@@ -1124,7 +1242,7 @@ export class Toolbar {
|
|
|
1124
1242
|
}
|
|
1125
1243
|
|
|
1126
1244
|
// Файл выбран - запускаем режим "призрака"
|
|
1127
|
-
|
|
1245
|
+
const fileSelectedData = {
|
|
1128
1246
|
file: file,
|
|
1129
1247
|
fileName: file.name,
|
|
1130
1248
|
fileSize: file.size,
|
|
@@ -1133,7 +1251,10 @@ export class Toolbar {
|
|
|
1133
1251
|
width: 120,
|
|
1134
1252
|
height: 140
|
|
1135
1253
|
}
|
|
1136
|
-
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
console.log('📁 Toolbar: эмитируем FileSelected:', fileSelectedData);
|
|
1257
|
+
this.eventBus.emit(Events.Place.FileSelected, fileSelectedData);
|
|
1137
1258
|
|
|
1138
1259
|
// Активируем инструмент размещения
|
|
1139
1260
|
this.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Загрузчик эмоджи для работы без bundler
|
|
3
|
+
* Заменяет import.meta.glob на динамическую загрузку
|
|
4
|
+
*/
|
|
5
|
+
export class EmojiLoaderNoBundler {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.emojiCache = new Map();
|
|
8
|
+
this.basePath = '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Инициализация с базовым путем
|
|
13
|
+
* @param {string} basePath - путь к папке с эмоджи (например: '/node_modules/moodboard-futurello/')
|
|
14
|
+
*/
|
|
15
|
+
init(basePath = '') {
|
|
16
|
+
this.basePath = basePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Загружает список эмоджи из статичного индекса
|
|
21
|
+
* @returns {Promise<Map>} карта категорий и эмоджи
|
|
22
|
+
*/
|
|
23
|
+
async loadEmojis() {
|
|
24
|
+
try {
|
|
25
|
+
// Попытка загрузить индекс эмоджи из JSON файла
|
|
26
|
+
const response = await fetch(`${this.basePath}src/assets/emodji/index.json`);
|
|
27
|
+
if (response.ok) {
|
|
28
|
+
const emojiIndex = await response.json();
|
|
29
|
+
return this.processEmojiIndex(emojiIndex);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('⚠️ Не удалось загрузить индекс эмоджи, используем fallback');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback: используем статичный список популярных эмоджи
|
|
36
|
+
return this.getFallbackEmojis();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Обрабатывает индекс эмоджи
|
|
41
|
+
* @param {Object} emojiIndex - индекс эмоджи из JSON
|
|
42
|
+
*/
|
|
43
|
+
processEmojiIndex(emojiIndex) {
|
|
44
|
+
const groups = new Map();
|
|
45
|
+
|
|
46
|
+
Object.entries(emojiIndex).forEach(([category, emojis]) => {
|
|
47
|
+
const emojiList = emojis.map(emoji => ({
|
|
48
|
+
path: `${this.basePath}src/assets/emodji/${category}/${emoji.file}`,
|
|
49
|
+
url: `${this.basePath}src/assets/emodji/${category}/${emoji.file}`
|
|
50
|
+
}));
|
|
51
|
+
groups.set(category, emojiList);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return groups;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Возвращает fallback список эмоджи
|
|
59
|
+
*/
|
|
60
|
+
getFallbackEmojis() {
|
|
61
|
+
const groups = new Map();
|
|
62
|
+
|
|
63
|
+
// Статичный список популярных эмоджи
|
|
64
|
+
const fallbackEmojis = {
|
|
65
|
+
'Смайлики': [
|
|
66
|
+
'1f600.png', '1f601.png', '1f602.png', '1f603.png', '1f604.png',
|
|
67
|
+
'1f605.png', '1f606.png', '1f607.png', '1f608.png', '1f609.png',
|
|
68
|
+
'1f60a.png', '1f60b.png', '1f60c.png', '1f60d.png', '1f60e.png',
|
|
69
|
+
'1f60f.png', '1f610.png', '1f611.png', '1f612.png', '1f613.png',
|
|
70
|
+
'1f614.png', '1f615.png', '1f616.png', '1f617.png', '1f618.png',
|
|
71
|
+
'1f619.png', '1f61a.png', '1f61b.png', '1f61c.png', '1f61d.png',
|
|
72
|
+
'1f61e.png', '1f61f.png', '1f620.png', '1f621.png', '1f622.png'
|
|
73
|
+
],
|
|
74
|
+
'Жесты': [
|
|
75
|
+
'1f44d.png', '1f44e.png', '1f44f.png', '1f450.png', '1f451.png',
|
|
76
|
+
'1f590.png', '270c.png', '1f91d.png', '1f64f.png', '1f44c.png'
|
|
77
|
+
],
|
|
78
|
+
'Разное': [
|
|
79
|
+
'2764.png', '1f494.png', '1f49c.png', '1f49a.png', '1f495.png',
|
|
80
|
+
'1f496.png', '1f497.png', '1f498.png', '1f499.png', '1f49b.png'
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
Object.entries(fallbackEmojis).forEach(([category, emojis]) => {
|
|
85
|
+
const emojiList = emojis.map(file => ({
|
|
86
|
+
path: `${this.basePath}src/assets/emodji/${category}/${file}`,
|
|
87
|
+
url: `${this.basePath}src/assets/emodji/${category}/${file}`
|
|
88
|
+
}));
|
|
89
|
+
groups.set(category, emojiList);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return groups;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Создает индексный JSON файл из существующих эмоджи
|
|
97
|
+
* Эта функция должна быть вызвана в dev режиме для генерации индекса
|
|
98
|
+
*/
|
|
99
|
+
async generateEmojiIndex() {
|
|
100
|
+
// Эта функция может быть вызвана только в dev среде с bundler
|
|
101
|
+
if (typeof import.meta !== 'undefined' && import.meta.glob) {
|
|
102
|
+
const modules = import.meta.glob('../assets/emodji/**/*.{png,PNG}', { eager: true, as: 'url' });
|
|
103
|
+
const index = {};
|
|
104
|
+
|
|
105
|
+
Object.keys(modules).forEach(path => {
|
|
106
|
+
const match = path.match(/\/emodji\/([^\/]+)\/([^\/]+)$/);
|
|
107
|
+
if (match) {
|
|
108
|
+
const [, category, file] = match;
|
|
109
|
+
if (!index[category]) index[category] = [];
|
|
110
|
+
index[category].push({ file });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('📁 Индекс эмоджи:', JSON.stringify(index, null, 2));
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -67,13 +67,19 @@ export function emojiToLocalUrl(emoji) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Карта локальных изображений (SVG и PNG) из src/assets/emodji
|
|
71
|
-
//
|
|
72
|
-
const _localEmojiModules =
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
// Карта локальных изображений (SVG и PNG) из src/assets/emodji
|
|
71
|
+
// В режиме с bundler используем import.meta.glob, иначе fallback
|
|
72
|
+
const _localEmojiModules = (() => {
|
|
73
|
+
if (typeof import.meta !== 'undefined' && import.meta.glob) {
|
|
74
|
+
try {
|
|
75
|
+
return import.meta.glob('../assets/emodji/**/*.{svg,SVG,png,PNG}', { eager: true, query: '?url', import: 'default' });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn('⚠️ import.meta.glob failed, using fallback for emoji resolution');
|
|
78
|
+
return {};
|
|
75
79
|
}
|
|
76
|
-
|
|
80
|
+
}
|
|
81
|
+
return {};
|
|
82
|
+
})();
|
|
77
83
|
|
|
78
84
|
// Индекс по имени файла (без пути)
|
|
79
85
|
const _localEmojiIndex = (() => {
|
|
@@ -119,3 +125,44 @@ export function buildLocalPaths(emoji) {
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Возвращает абсолютный URL для эмоджи с учетом базового пути
|
|
130
|
+
* @param {string} emoji - эмоджи символ
|
|
131
|
+
* @param {string} basePath - базовый путь к ассетам
|
|
132
|
+
* @returns {string|null} абсолютный URL или null
|
|
133
|
+
*/
|
|
134
|
+
export function resolveEmojiAbsoluteUrl(emoji, basePath = null) {
|
|
135
|
+
try {
|
|
136
|
+
const base = emojiFilenameBase(emoji);
|
|
137
|
+
if (!base) return null;
|
|
138
|
+
|
|
139
|
+
// Определяем базовый путь
|
|
140
|
+
let resolvedBasePath = basePath;
|
|
141
|
+
|
|
142
|
+
if (!resolvedBasePath) {
|
|
143
|
+
// Пытаемся определить от import.meta.url
|
|
144
|
+
try {
|
|
145
|
+
resolvedBasePath = new URL('../assets/emodji/', import.meta.url).href;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Fallback на глобальную настройку
|
|
148
|
+
if (window.MOODBOARD_BASE_PATH) {
|
|
149
|
+
const globalPath = window.MOODBOARD_BASE_PATH.endsWith('/') ? window.MOODBOARD_BASE_PATH : window.MOODBOARD_BASE_PATH + '/';
|
|
150
|
+
resolvedBasePath = `${globalPath}src/assets/emodji/`;
|
|
151
|
+
} else {
|
|
152
|
+
resolvedBasePath = '/src/assets/emodji/';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Формируем URL (приоритет PNG, потом SVG)
|
|
158
|
+
if (!resolvedBasePath.endsWith('/')) resolvedBasePath += '/';
|
|
159
|
+
|
|
160
|
+
// Возвращаем URL в формате готовом для использования
|
|
161
|
+
return `${resolvedBasePath}Смайлики/${base}.png`; // Большинство эмоджи в папке Смайлики
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn('⚠️ Ошибка resolveEmojiAbsoluteUrl:', error);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Утилита для загрузки CSS стилей без bundler
|
|
3
|
+
* Подключает все необходимые стили для MoodBoard
|
|
4
|
+
*/
|
|
5
|
+
export class StyleLoader {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.loadedStyles = new Set();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Загружает все стили MoodBoard
|
|
12
|
+
* @param {string} basePath - базовый путь к node_modules или dist
|
|
13
|
+
*/
|
|
14
|
+
async loadAllStyles(basePath = '') {
|
|
15
|
+
const styles = [
|
|
16
|
+
'src/ui/styles/workspace.css',
|
|
17
|
+
'src/ui/styles/toolbar.css',
|
|
18
|
+
'src/ui/styles/topbar.css',
|
|
19
|
+
'src/ui/styles/panels.css'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
console.log('🎨 StyleLoader: Загружаем стили MoodBoard...');
|
|
23
|
+
|
|
24
|
+
for (const stylePath of styles) {
|
|
25
|
+
try {
|
|
26
|
+
await this.loadStyle(basePath + stylePath);
|
|
27
|
+
console.log(`✅ Стиль загружен: ${stylePath}`);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn(`⚠️ Ошибка загрузки стиля ${stylePath}:`, error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log('🎨 StyleLoader: Все стили загружены');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Загружает отдельный CSS файл
|
|
38
|
+
* @param {string} href - путь к CSS файлу
|
|
39
|
+
*/
|
|
40
|
+
async loadStyle(href) {
|
|
41
|
+
// Проверяем, не загружен ли уже этот стиль
|
|
42
|
+
if (this.loadedStyles.has(href)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
// Создаем link элемент
|
|
48
|
+
const link = document.createElement('link');
|
|
49
|
+
link.rel = 'stylesheet';
|
|
50
|
+
link.type = 'text/css';
|
|
51
|
+
link.href = href;
|
|
52
|
+
|
|
53
|
+
// Обработчики загрузки
|
|
54
|
+
link.onload = () => {
|
|
55
|
+
this.loadedStyles.add(href);
|
|
56
|
+
resolve();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
link.onerror = () => {
|
|
60
|
+
reject(new Error(`Failed to load CSS: ${href}`));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Добавляем в head
|
|
64
|
+
document.head.appendChild(link);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Загружает стили синхронно (для критичных стилей)
|
|
70
|
+
* @param {string} css - CSS код
|
|
71
|
+
* @param {string} id - уникальный ID для style элемента
|
|
72
|
+
*/
|
|
73
|
+
injectInlineStyles(css, id = 'moodboard-styles') {
|
|
74
|
+
// Проверяем, не загружен ли уже
|
|
75
|
+
if (document.getElementById(id)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const style = document.createElement('style');
|
|
80
|
+
style.id = id;
|
|
81
|
+
style.textContent = css;
|
|
82
|
+
document.head.appendChild(style);
|
|
83
|
+
}
|
|
84
|
+
}
|