leksy-editor 1.2.1 → 1.3.1

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/constant.js CHANGED
@@ -653,6 +653,10 @@ const TAB_CATEGORIES = {
653
653
 
654
654
  const RESIZE_MARGIN = 10;
655
655
 
656
+ const LIST_STYLES_BY_LEVEL = ['1', 'a', 'i', 'A'];
657
+ const UNORDERED_LIST_STYLES_BY_LEVEL = ['disc', 'circle', 'square'];
658
+
659
+
656
660
  export {
657
661
  ERRORS,
658
662
  CLASSES,
@@ -675,4 +679,6 @@ export {
675
679
  CSS_VARIABLES,
676
680
  TAB_CATEGORIES,
677
681
  RESIZE_MARGIN,
682
+ LIST_STYLES_BY_LEVEL,
683
+ UNORDERED_LIST_STYLES_BY_LEVEL,
678
684
  }
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import './style.css'
2
2
  import { CLASSES, CSS, CSS_VARIABLES, ERRORS, REGEX, SVG } from "./constant"
3
- import PLUGINS, { applyOrderList, applyTextFormat, applyUnorderedList } from './plugin';
4
- import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName, showRemoteCursor, syncRemoteChangesDebounce, applyRemoteChanges, updateCursorPositionDebounce, buildTributeValues, updateHeight, getTableGrid, getCellPosition, makeUnorderedList, makeOrderedList, makeHeading, makeBlockQuote, makeStrikethrough, makeCodeBlock } from './utilities';
3
+ import PLUGINS, { applyTextFormat } from './plugin';
4
+ import { showAnchorPopover, changeAllToolbarState, changeToolbarStateByName, changeToolbarValueByName, cleanHTML, debounce, destroyImageResizer, destroyTableEditPlugin, initImageResizer, initTableEditPlugin, makeToolbarButton, makeToolbarColor, makeToolbarDropdown, makeToolbarSelect, rgbToHex, updateTableResizerPosition, destroyAnchorPopover, changeToolbarHtmlByName, showRemoteCursor, syncRemoteChangesDebounce, applyRemoteChanges, updateCursorPositionDebounce, buildTributeValues, updateHeight, getTableGrid, getCellPosition, makeUnorderedList, makeOrderedList, makeHeading, makeBlockQuote, makeStrikethrough, makeCodeBlock, makeSublist, showTooltip } from './utilities';
5
5
 
6
6
  class LeksyEditor {
7
7
 
@@ -386,6 +386,7 @@ class LeksyEditor {
386
386
  })
387
387
 
388
388
  if (options.disabled) changeAllToolbarState(core, 'disabled')
389
+ showTooltip(options)
389
390
 
390
391
  core.elements.toolbarContainer = toolbarContainer;
391
392
  core.elements.base.appendChild(toolbarContainer);
@@ -725,6 +726,9 @@ class LeksyEditor {
725
726
  }
726
727
 
727
728
  if (event.key === 'Tab') {
729
+ makeSublist(event, core);
730
+ if (event.defaultPrevented) return;
731
+
728
732
  event.preventDefault();
729
733
 
730
734
  const tabNode = document.createTextNode("\u00a0\u00a0\u00a0\u00a0");
@@ -816,7 +820,7 @@ class LeksyEditor {
816
820
 
817
821
  if (e.target.closest("a")?.tagName === 'A') {
818
822
  const anchor = e.target.closest("a");
819
- if (anchor) {
823
+ if (anchor && element.tagName !== 'IMG') {
820
824
  core.elements.selectedElement = anchor;
821
825
  showAnchorPopover(anchor, e.pageX, e.pageY, options, core);
822
826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leksy-editor",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Leksy Editor is an alternative to traditional WYSIWYG editors, designed primarily for creating mail templates, blogs, and documents without any content manipulation.",
5
5
  "main": "index.js",
6
6
  "directories": {
package/plugin.js CHANGED
@@ -233,7 +233,7 @@ const PLUGINS = {
233
233
  }
234
234
  },
235
235
  'font-size': {
236
- title: 'Font',
236
+ title: 'Font Size',
237
237
  icon: 'Font Size',
238
238
  type: 'select',
239
239
  options: Object.keys(FONT_SIZE_OPTIONS).map(size => ({
@@ -930,6 +930,7 @@ const PLUGINS = {
930
930
  if (url) {
931
931
  const a = document.createElement('a');
932
932
  a.href = url.videoUrl
933
+ a.dataset.leksyPreview = 'video-url'
933
934
  a.target = '_blank'
934
935
  const img = document.createElement('img');
935
936
  img.src = url.thumbnailUrl
@@ -946,6 +947,7 @@ const PLUGINS = {
946
947
  if (url) {
947
948
  const a = document.createElement('a');
948
949
  a.href = url.videoUrl
950
+ a.dataset.leksyPreview = 'video-url'
949
951
  a.target = '_blank'
950
952
  const img = document.createElement('img');
951
953
  img.src = url.thumbnailUrl
package/style.css CHANGED
@@ -152,7 +152,9 @@
152
152
 
153
153
  .leksy-editor-toolbar-item::after {
154
154
  content: attr(data-title);
155
- position: absolute;
155
+ position: fixed;
156
+ top: var(--leksy-editor-top, 0);
157
+ left: var(--leksy-editor-left, 0);
156
158
  opacity: 0;
157
159
  visibility: hidden;
158
160
  transition: opacity 0.3s, visibility 0.3s;
@@ -161,8 +163,6 @@
161
163
  color: black;
162
164
  padding: 5px 10px;
163
165
  border-radius: 5px;
164
- bottom: -90%;
165
- left: 60%;
166
166
  transform: translateX(-50%);
167
167
  white-space: nowrap;
168
168
  z-index: 1;
@@ -516,7 +516,6 @@ button.leksy-editor-popover-tab.active {
516
516
  height: 2rem;
517
517
  padding: 0.75rem 1rem;
518
518
  background-color: hsl(160, 100%, 35%);
519
- border-color: hsl(160, 75%, 50%);
520
519
  display: flex;
521
520
  align-items: center;
522
521
  justify-content: center;
@@ -530,7 +529,6 @@ button.leksy-editor-popover-tab.active {
530
529
  white-space: nowrap;
531
530
  gap: 0.5rem;
532
531
  transition: 500ms;
533
- text-transform: uppercase;
534
532
  font-size: 1rem;
535
533
  }
536
534
 
package/utilities.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS, TAB_CATEGORIES, RESIZE_MARGIN } from "./constant";
1
+ import { CLASSES, FONT_SIZES, FONTS, FORMATS, GIPHY_POWERED_IMAGE, REGEX, RESIZER_PLUGINS, SOCIAL_MEDIA_BASEURLS, SOCIAL_MEDIA_PATTERNS, SVG, TABLE_PLUGINS, TAB_CATEGORIES, RESIZE_MARGIN, LIST_STYLES_BY_LEVEL, UNORDERED_LIST_STYLES_BY_LEVEL } from "./constant";
2
2
  import { DiffDOM } from 'diff-dom';
3
3
  import { applyUnorderedList, applyOrderList } from "./plugin";
4
4
 
@@ -161,6 +161,24 @@ function getScrollParent(element) {
161
161
  return document.body;
162
162
  }
163
163
 
164
+ const showTooltip = (options) => {
165
+ document.addEventListener('mouseover', (e) => {
166
+ const el = e.target.closest(`.${options.classPrefix}${CLASSES.TOOLBAR_ITEM}`);
167
+ if (!el) return;
168
+
169
+ const rect = el.getBoundingClientRect();
170
+
171
+ let top = rect.bottom + 2;
172
+ let left = rect.left + rect.width / 2;
173
+
174
+ left = Math.max(68, Math.min(left, window.innerWidth - 68));
175
+
176
+ el.style.setProperty('--leksy-editor-top', `${top}px`);
177
+ el.style.setProperty('--leksy-editor-left', `${left}px`);
178
+ });
179
+ };
180
+
181
+
164
182
  const makeToolbarButton = (_plugin, options, core) => {
165
183
  const pluginButton = document.createElement('button');
166
184
  pluginButton.type = "button"
@@ -939,6 +957,7 @@ const makeEditToolbar = (options, core, { type, td, image, updateImage, updateTa
939
957
 
940
958
  Object.keys(TAB_CATEGORIES).forEach((categoryName, index) => {
941
959
  const tabButton = document.createElement('button');
960
+ tabButton.type = 'button'
942
961
  tabButton.textContent = categoryName;
943
962
  tabButton.className = `${options.classPrefix}${CLASSES.POPOVER_TAB}`
944
963
 
@@ -1098,7 +1117,18 @@ const initImageResizer = (type, image, options, core) => {
1098
1117
  fullscreenButton.style.justifyContent = 'center';
1099
1118
 
1100
1119
  fullscreenButton.addEventListener('click', () => {
1101
- createPreviewModal(image.src, core, options); // Open custom modal with image preview
1120
+ const anchor = image.closest('a');
1121
+ let isVideo = false;
1122
+ let src = image.src;
1123
+
1124
+ if (anchor?.dataset?.leksyPreview === 'video-url') {
1125
+ const href = anchor.getAttribute('href');
1126
+ if (href) {
1127
+ isVideo = true;
1128
+ src = href;
1129
+ }
1130
+ }
1131
+ createPreviewModal(src, core, options, isVideo);
1102
1132
  });
1103
1133
 
1104
1134
  resizer.appendChild(fullscreenButton);
@@ -1318,7 +1348,7 @@ const initImageResizer = (type, image, options, core) => {
1318
1348
  core.elements.iframeWindow.addEventListener('keydown', core.deleteImageHandler)
1319
1349
  }
1320
1350
 
1321
- const createPreviewModal = (imageSrc, core, options) => {
1351
+ const createPreviewModal = (src, core, options, isVideo = false) => {
1322
1352
  // Create overlay
1323
1353
  const overlay = document.createElement('div');
1324
1354
  overlay.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_OVERLAY} `
@@ -1337,12 +1367,22 @@ const createPreviewModal = (imageSrc, core, options) => {
1337
1367
  const modalBody = document.createElement('div');
1338
1368
  modalBody.className = `${options.classPrefix}${CLASSES.PREVIEW_MODAL_BODY} `
1339
1369
 
1340
- // Image element
1341
- const image = document.createElement('img');
1342
- image.src = imageSrc;
1343
- image.alt = 'Preview Image';
1370
+ let element;
1371
+ if (isVideo) {
1372
+ element = document.createElement('video');
1373
+ element.src = src;
1374
+ element.controls = true;
1375
+ element.style.maxWidth = '100%';
1376
+ element.style.maxHeight = '100%';
1377
+ element.setAttribute('controlsList', 'nodownload');
1378
+ element.setAttribute('disablePictureInPicture', true);
1379
+ } else {
1380
+ element = document.createElement('img');
1381
+ element.src = src;
1382
+ element.alt = 'Preview Image';
1383
+ }
1344
1384
 
1345
- modalBody.appendChild(image);
1385
+ modalBody.appendChild(element);
1346
1386
  modalContent.append(closeButton, modalBody);
1347
1387
  overlay.appendChild(modalContent);
1348
1388
 
@@ -1350,7 +1390,7 @@ const createPreviewModal = (imageSrc, core, options) => {
1350
1390
  document.body.appendChild(overlay);
1351
1391
 
1352
1392
  overlay.addEventListener('click', (event) => {
1353
- if (event.target !== image) {
1393
+ if (event.target !== element) {
1354
1394
  overlay.remove();
1355
1395
  }
1356
1396
  });
@@ -2927,6 +2967,112 @@ const makeCodeBlock = (event, core) => {
2927
2967
  core.updateCaretPosition();
2928
2968
  }
2929
2969
 
2970
+ const getListLevel = (li) => {
2971
+ let level = 0;
2972
+ let parent = li.parentElement;
2973
+ const tagName = parent.tagName;
2974
+
2975
+ while (parent && parent.tagName === tagName) {
2976
+ level++;
2977
+ parent = parent.parentElement?.closest(tagName);
2978
+ }
2979
+
2980
+ return level;
2981
+ };
2982
+
2983
+ const applyListStyle = (list, level) => {
2984
+ let type;
2985
+ if (list.tagName === 'OL') {
2986
+ type = LIST_STYLES_BY_LEVEL[(level - 1) % LIST_STYLES_BY_LEVEL.length];
2987
+ } else if (list.tagName === 'UL') {
2988
+ type = UNORDERED_LIST_STYLES_BY_LEVEL[(level - 1) % UNORDERED_LIST_STYLES_BY_LEVEL.length];
2989
+ }
2990
+ if (type) list.setAttribute('type', type);
2991
+ };
2992
+
2993
+ const indentListItem = (li, core) => {
2994
+ const prevLi = li.previousElementSibling;
2995
+ if (!prevLi) return;
2996
+
2997
+ const parentTag = li.parentElement.tagName;
2998
+ let subList = prevLi.querySelector(parentTag);
2999
+
3000
+ if (!subList) {
3001
+ subList = document.createElement(parentTag);
3002
+ prevLi.appendChild(subList);
3003
+ }
3004
+
3005
+ subList.appendChild(li);
3006
+ const selection = core.elements.iframeWindow.getSelection();
3007
+ const range = document.createRange();
3008
+ range.selectNodeContents(li);
3009
+ range.collapse(false);
3010
+ selection.removeAllRanges();
3011
+ selection.addRange(range);
3012
+ core.elements.editor.focus();
3013
+ core.updateCaretPosition();
3014
+
3015
+ if (parentTag === 'OL' || parentTag === 'UL') {
3016
+ const level = getListLevel(li);
3017
+ applyListStyle(subList, level);
3018
+ }
3019
+ };
3020
+
3021
+ const outdentListItem = (li, core) => {
3022
+ const parentOl = li.parentElement;
3023
+ const parentLi = parentOl.closest('li');
3024
+ if (!parentLi) return;
3025
+
3026
+ parentLi.after(li);
3027
+
3028
+ const selection = core.elements.iframeWindow.getSelection();
3029
+ const range = document.createRange();
3030
+ range.selectNodeContents(li);
3031
+ range.collapse(false);
3032
+ selection.removeAllRanges();
3033
+ selection.addRange(range);
3034
+ core.elements.editor.focus();
3035
+ core.updateCaretPosition();
3036
+
3037
+
3038
+ if (parentOl.children.length === 0) {
3039
+ parentOl.remove();
3040
+ }
3041
+
3042
+ if (li.parentElement.tagName === 'OL' || li.parentElement.tagName === 'UL') {
3043
+ const level = getListLevel(li);
3044
+ applyListStyle(li.parentElement, level);
3045
+ }
3046
+ };
3047
+
3048
+ const makeSublist = (event, core) => {
3049
+ if (event.key !== 'Tab') return;
3050
+
3051
+ const selection = core.elements.iframeWindow.getSelection();
3052
+ if (!selection.rangeCount) return;
3053
+
3054
+ let node = selection.anchorNode;
3055
+ if (node.nodeType === Node.TEXT_NODE) {
3056
+ node = node.parentElement;
3057
+ }
3058
+
3059
+ const li = node.closest('li');
3060
+ if (!li) return;
3061
+
3062
+ const clone = li.cloneNode(true);
3063
+ clone.querySelectorAll('ul, ol').forEach(l => l.remove());
3064
+ const hasContent = clone.textContent.replace(/\u00A0/g, '').length > 0 || clone.querySelector('img, video, audio, table, hr');
3065
+ if (hasContent) return;
3066
+
3067
+ event.preventDefault();
3068
+
3069
+ if (event.shiftKey) {
3070
+ outdentListItem(li, core);
3071
+ } else {
3072
+ indentListItem(li, core);
3073
+ }
3074
+ };
3075
+
2930
3076
  export {
2931
3077
  cleanHTML,
2932
3078
  debounce,
@@ -2962,8 +3108,10 @@ export {
2962
3108
  getCellPosition,
2963
3109
  makeUnorderedList,
2964
3110
  makeOrderedList,
3111
+ showTooltip,
2965
3112
  makeHeading,
2966
3113
  makeBlockQuote,
2967
3114
  makeStrikethrough,
2968
3115
  makeCodeBlock,
3116
+ makeSublist,
2969
3117
  }