@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -1,2 +1,7 @@
1
- // Единственный экспорт пакета - готовый MoodBoard с UI
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
- this.showFileGhost();
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
- if (!this.selectedFile || !this.world) return;
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
- const pendingFont = (this.pending.properties?.fontFamily) || 'Caveat, Arial, cursive';
567
- const primaryFont = String(pendingFont).split(',')[0].trim().replace(/^['"]|['"]$/g, '') || 'Caveat';
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 maxWidth = this.selectedImage.properties.width || 300;
727
- const maxHeight = this.selectedImage.properties.height || 200;
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
- const maxWidth = this.pending.size?.width || this.pending.properties?.width || 56;
820
- const maxHeight = this.pending.size?.height || this.pending.properties?.height || 56;
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
- try {
858
- if (this.app && this.app.view && src) {
859
- const cursorSize = 24;
860
- const url = encodeURI(src);
861
- // Используем CSS cursor с изображением, если поддерживается
862
- this.app.view.style.cursor = `url(${url}) ${Math.floor(cursorSize/2)} ${Math.floor(cursorSize/2)}, default`;
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
- } catch (_) {}
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
- // Группируем по подпапкам внутри emodji (категории)
913
- const entries = Object.entries(modules).sort(([a], [b]) => a.localeCompare(b));
914
- const groups = new Map();
915
- entries.forEach(([path, url]) => {
916
- const marker = '/emodji/';
917
- const idx = path.indexOf(marker);
918
- let category = 'Разное';
919
- if (idx >= 0) {
920
- const after = path.slice(idx + marker.length);
921
- const parts = after.split('/');
922
- category = parts.length > 1 ? parts[0] : 'Разное';
923
- }
924
- if (!groups.has(category)) groups.set(category, []);
925
- groups.get(category).push({ path, url });
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
- // Снимем флаг сразу после клика, чтобы click мог отфильтроваться
989
- setTimeout(() => { btn.__dragActive = false; }, 0);
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
- if (btn.__dragActive) return; // не обрабатываем клик после drag
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
- this.eventBus.emit(Events.Place.FileSelected, {
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 (собирается Vite'ом)
71
- // Ключи вида '../assets/emodji/1f600.svg' / '../assets/emodji/1f600.png' URL
72
- const _localEmojiModules = import.meta && typeof import.meta.glob === 'function'
73
- ? {
74
- ...import.meta.glob('../assets/emodji/**/*.{svg,SVG,png,PNG}', { eager: true, query: '?url', import: 'default' })
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
+ }