@diplodoc/transform 4.70.3 → 4.71.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.
Files changed (42) hide show
  1. package/dist/css/_yfm-only.css +4 -4
  2. package/dist/css/_yfm-only.css.map +3 -3
  3. package/dist/css/_yfm-only.min.css +1 -1
  4. package/dist/css/_yfm-only.min.css.map +3 -3
  5. package/dist/css/base.css +1 -0
  6. package/dist/css/base.css.map +3 -3
  7. package/dist/css/base.min.css +1 -1
  8. package/dist/css/base.min.css.map +3 -3
  9. package/dist/css/print.css.map +1 -1
  10. package/dist/css/yfm.css +6 -4
  11. package/dist/css/yfm.css.map +3 -3
  12. package/dist/css/yfm.min.css +1 -1
  13. package/dist/css/yfm.min.css.map +3 -3
  14. package/dist/js/base.js +396 -215
  15. package/dist/js/base.js.map +4 -4
  16. package/dist/js/base.min.js +1 -6
  17. package/dist/js/base.min.js.map +4 -4
  18. package/dist/js/yfm.js +429 -228
  19. package/dist/js/yfm.js.map +4 -4
  20. package/dist/js/yfm.min.js +1 -6
  21. package/dist/js/yfm.min.js.map +4 -4
  22. package/dist/scss/_common.scss +1 -0
  23. package/dist/scss/_modal.scss +1 -0
  24. package/dist/scss/{_inline-code.scss → _tooltip.scss} +1 -1
  25. package/dist/scss/_yfm-only.scss +1 -1
  26. package/lib/plugins/anchors/index.js +1 -1
  27. package/lib/plugins/anchors/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/js/anchor.ts +27 -3
  30. package/src/js/{inline-code/constant.ts → constant.ts} +1 -9
  31. package/src/js/inline-code/index.ts +24 -42
  32. package/src/js/tooltip/constant.ts +25 -0
  33. package/src/js/tooltip/index.ts +2 -0
  34. package/src/js/tooltip/tooltip.ts +263 -0
  35. package/src/js/tooltip/types.ts +59 -0
  36. package/src/js/tooltip/utils.ts +247 -0
  37. package/src/scss/_common.scss +1 -0
  38. package/src/scss/_modal.scss +1 -0
  39. package/src/scss/{_inline-code.scss → _tooltip.scss} +1 -1
  40. package/src/scss/_yfm-only.scss +1 -1
  41. package/src/transform/plugins/anchors/index.ts +1 -1
  42. package/src/js/inline-code/utils.ts +0 -217
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diplodoc/transform",
3
- "version": "4.70.3",
3
+ "version": "4.71.0",
4
4
  "description": "A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML",
5
5
  "keywords": [
6
6
  "markdown",
package/src/js/anchor.ts CHANGED
@@ -1,8 +1,28 @@
1
+ import type {Lang} from 'src/transform/typings';
2
+
3
+ import {createTooltipFactory} from './tooltip';
1
4
  import {copyToClipboard, getEventTarget, isCustom} from './utils';
5
+ import {COPIED_LANG_TOKEN} from './constant';
2
6
 
7
+ const ALLOWED_PROTOCOL_RE = /^(?:https?|file):$/;
3
8
  const ANCHOR_BUTTON_SELECTOR = '.yfm-clipboard-anchor';
4
9
 
10
+ const tooltip = createTooltipFactory();
11
+
12
+ function getLink(target: HTMLElement) {
13
+ const href = target.nodeName === 'A' ? (target as HTMLAnchorElement).href : target.dataset.href;
14
+ const link = new URL(href || '', window.location.href);
15
+
16
+ if (ALLOWED_PROTOCOL_RE.test(link.protocol)) {
17
+ return link.href;
18
+ }
19
+
20
+ return window.location.href;
21
+ }
22
+
5
23
  if (typeof document !== 'undefined') {
24
+ tooltip.init();
25
+
6
26
  document.addEventListener('click', (event) => {
7
27
  const target = getEventTarget(event) as HTMLElement;
8
28
 
@@ -10,9 +30,13 @@ if (typeof document !== 'undefined') {
10
30
  return;
11
31
  }
12
32
 
13
- const href = target.getAttribute('data-href') || '';
14
- const link = new URL(href, window.location.href).toString();
33
+ const link = getLink(target);
34
+
35
+ copyToClipboard(link).then(() => {
36
+ const lang = document.documentElement.lang || 'en';
37
+ const tooltipText = COPIED_LANG_TOKEN[lang as Lang] ?? COPIED_LANG_TOKEN.en;
15
38
 
16
- copyToClipboard(link);
39
+ tooltip.show(target, tooltipText);
40
+ });
17
41
  });
18
42
  }
@@ -1,14 +1,6 @@
1
1
  import type {Lang} from 'src/transform/typings';
2
2
 
3
- export const INLINE_CODE = '.yfm-clipboard-inline-code';
4
-
5
- export const INLINE_CODE_ID = 'tooltip_inline_clipboard_dialog';
6
-
7
- export const INLINE_CODE_CLASS = 'yfm inline_code_tooltip';
8
-
9
- export const OPEN_CLASS = 'open';
10
-
11
- export const LANG_TOKEN: Record<Lang, string> = {
3
+ export const COPIED_LANG_TOKEN: Record<Lang, string> = {
12
4
  ru: 'Скопировано',
13
5
  en: 'Copied',
14
6
  ar: 'تم النسخ',
@@ -1,13 +1,16 @@
1
+ import type {Lang} from 'src/transform/typings';
2
+
3
+ import {createTooltipFactory} from '../tooltip';
1
4
  import {copyToClipboard, getEventTarget, isCustom} from '../utils';
5
+ import {COPIED_LANG_TOKEN} from '../constant';
6
+
7
+ const CLASS_INLINE_CODE = 'yfm-clipboard-inline-code';
8
+ const INLINE_CODE = `.${CLASS_INLINE_CODE}`;
2
9
 
3
- import {INLINE_CODE, OPEN_CLASS} from './constant';
4
- import {
5
- closeTooltip,
6
- getInlineCodeByTooltip,
7
- getTooltipElement,
8
- setTooltipPosition,
9
- tooltipWorker,
10
- } from './utils';
10
+ const tooltip = createTooltipFactory({
11
+ // NOTE: Add additional className for backward capability
12
+ additionalClassName: 'inline_code_tooltip',
13
+ });
11
14
 
12
15
  export function inlineCopyFn(target: HTMLElement) {
13
16
  const innerText = target.innerText;
@@ -17,14 +20,18 @@ export function inlineCopyFn(target: HTMLElement) {
17
20
  }
18
21
 
19
22
  copyToClipboard(innerText).then(() => {
20
- tooltipWorker(target);
23
+ const lang = document.documentElement.lang || 'en';
24
+ const tooltipText = COPIED_LANG_TOKEN[lang as Lang] ?? COPIED_LANG_TOKEN.en;
25
+
26
+ tooltip.show(target, tooltipText);
21
27
  });
22
28
  }
23
29
 
24
30
  if (typeof document !== 'undefined') {
31
+ tooltip.init();
32
+
25
33
  document.addEventListener('click', (event) => {
26
34
  const target = getEventTarget(event) as HTMLElement;
27
-
28
35
  const inline = target.matches(INLINE_CODE);
29
36
 
30
37
  if (isCustom(event) || !inline) {
@@ -37,45 +44,20 @@ if (typeof document !== 'undefined') {
37
44
  document.addEventListener('keydown', (event) => {
38
45
  if (event.key === 'Enter' && document.activeElement) {
39
46
  const activeElement = document.activeElement as HTMLElement;
40
- const classInlineCode = INLINE_CODE.replace('.', '');
41
-
42
- if (!activeElement.classList.contains(classInlineCode)) {
43
- return;
44
- }
45
-
46
- const innerText = activeElement.innerText;
47
47
 
48
- if (!innerText) {
48
+ if (!activeElement.classList.contains(CLASS_INLINE_CODE)) {
49
49
  return;
50
50
  }
51
51
 
52
- copyToClipboard(innerText).then(() => {
53
- tooltipWorker(activeElement);
54
- });
52
+ inlineCopyFn(activeElement);
55
53
  }
56
54
 
57
- const inlineTooltip = getTooltipElement();
55
+ if (event.key === 'Escape' && tooltip.visible) {
56
+ const reference = tooltip.getActiveReference();
58
57
 
59
- if (event.key === 'Escape' && inlineTooltip) {
60
- closeTooltip(inlineTooltip);
61
- getInlineCodeByTooltip(inlineTooltip)?.focus(); // Set focus back to open button after closing popup
58
+ tooltip.hide();
59
+ // Set focus back to open button after closing popup
60
+ reference?.focus();
62
61
  }
63
62
  });
64
-
65
- window.addEventListener('resize', () => {
66
- const inlineTooltip = getTooltipElement();
67
- if (!inlineTooltip) {
68
- return;
69
- }
70
-
71
- const inlineId = inlineTooltip.getAttribute('inline-id') || '';
72
- const inlineCodeElement = document.getElementById(inlineId);
73
-
74
- if (!inlineCodeElement) {
75
- inlineTooltip.classList.toggle(OPEN_CLASS);
76
- return;
77
- }
78
-
79
- setTooltipPosition(inlineTooltip, inlineCodeElement);
80
- });
81
63
  }
@@ -0,0 +1,25 @@
1
+ import type {OffsetValues, Side, SideObject} from './types';
2
+
3
+ export const PAGE_CONTAINER_SELECTOR = '.dc-doc-page__content';
4
+
5
+ export const TOOLTIP_BASE_CLASS = 'yfm yfm-tooltip';
6
+
7
+ export const TOOLTIP_OPEN_CLASS = 'open';
8
+
9
+ export const TOOLTIP_DATA_ATTR = 'data-tooltip-id';
10
+
11
+ export const DEFAULT_OFFSET_VALUES: OffsetValues = {mainAxis: 5};
12
+
13
+ export const OPPOSITE_SIDES: Record<Side, Side> = {
14
+ top: 'bottom',
15
+ bottom: 'top',
16
+ left: 'right',
17
+ right: 'left',
18
+ };
19
+
20
+ export const DEFAULT_SIDE_OBJECT: SideObject = {
21
+ top: 0,
22
+ bottom: 0,
23
+ left: 0,
24
+ right: 0,
25
+ };
@@ -0,0 +1,2 @@
1
+ export type {TooltipOptions} from './types';
2
+ export {createTooltipFactory} from './tooltip';
@@ -0,0 +1,263 @@
1
+ import type {
2
+ Coords,
3
+ TooltipContext,
4
+ TooltipContextElements,
5
+ TooltipElementOptions,
6
+ TooltipOptions,
7
+ } from './types';
8
+
9
+ import {
10
+ DEFAULT_OFFSET_VALUES,
11
+ PAGE_CONTAINER_SELECTOR,
12
+ TOOLTIP_BASE_CLASS,
13
+ TOOLTIP_DATA_ATTR,
14
+ TOOLTIP_OPEN_CLASS,
15
+ } from './constant';
16
+ import {
17
+ computePosition,
18
+ convertToRelativeToOffsetParentRect,
19
+ createRect,
20
+ generateId,
21
+ getElementRect,
22
+ getViewportRect,
23
+ updateRect,
24
+ } from './utils';
25
+
26
+ interface TooltipState {
27
+ currentId: string | null;
28
+ timer: ReturnType<typeof setTimeout> | null;
29
+ unsubscribe: (() => void) | null;
30
+ }
31
+
32
+ export function createTooltipFactory(options: TooltipOptions = {}) {
33
+ const {closeDelay = 1_000, additionalClassName} = options;
34
+ let initialized = false;
35
+
36
+ const state: TooltipState = {
37
+ currentId: null,
38
+ timer: null,
39
+ unsubscribe: null,
40
+ };
41
+
42
+ const getActiveTooltip = () => {
43
+ if (!state.currentId) {
44
+ return null;
45
+ }
46
+
47
+ return document.getElementById(state.currentId);
48
+ };
49
+
50
+ const getActiveReference = () => {
51
+ if (!state.currentId) {
52
+ return null;
53
+ }
54
+
55
+ return getReferenceByTooltipId(state.currentId);
56
+ };
57
+
58
+ const hide = () => {
59
+ const tooltip = getActiveTooltip();
60
+
61
+ if (state.timer) {
62
+ clearTimeout(state.timer);
63
+ state.timer = null;
64
+ }
65
+
66
+ if (state.unsubscribe) {
67
+ state.unsubscribe();
68
+ state.unsubscribe = null;
69
+ }
70
+
71
+ if (tooltip) {
72
+ tooltip.classList.remove(TOOLTIP_OPEN_CLASS);
73
+ detachTooltip(tooltip);
74
+
75
+ state.currentId = null;
76
+ }
77
+ };
78
+
79
+ const show = (reference: HTMLElement, text: string) => {
80
+ hide();
81
+
82
+ const tooltip = createTooltipElement({text, className: additionalClassName});
83
+ const update = updateTooltipPosition.bind(null, options, reference, tooltip);
84
+
85
+ state.currentId = tooltip.id;
86
+
87
+ attachTooltip(tooltip, reference);
88
+
89
+ state.unsubscribe = subscribeToScroll(reference, update);
90
+
91
+ tooltip.classList.add(TOOLTIP_OPEN_CLASS);
92
+
93
+ update();
94
+
95
+ if (closeDelay > 0) {
96
+ state.timer = setTimeout(hide, closeDelay);
97
+ }
98
+ };
99
+
100
+ const handleUpdate = () => {
101
+ const activeTooltip = getActiveTooltip();
102
+ const activeReference = getActiveReference();
103
+
104
+ if (activeTooltip && !activeReference) {
105
+ hide();
106
+ return;
107
+ }
108
+
109
+ if (activeTooltip && activeReference) {
110
+ updateTooltipPosition(options, activeReference, activeTooltip);
111
+ }
112
+ };
113
+
114
+ const init = () => {
115
+ if (!initialized) {
116
+ initialized = true;
117
+
118
+ window.addEventListener('scroll', handleUpdate);
119
+ window.addEventListener('resize', handleUpdate);
120
+ }
121
+ };
122
+
123
+ const cleanup = () => {
124
+ if (initialized) {
125
+ initialized = false;
126
+
127
+ window.removeEventListener('scroll', handleUpdate);
128
+ window.removeEventListener('resize', handleUpdate);
129
+ }
130
+ };
131
+
132
+ return {
133
+ get visible() {
134
+ return Boolean(state.currentId);
135
+ },
136
+ getActiveReference,
137
+ show,
138
+ hide,
139
+ init,
140
+ cleanup,
141
+ };
142
+ }
143
+
144
+ function createTooltipElement(options: TooltipElementOptions) {
145
+ const {text, className} = options;
146
+
147
+ const id = generateId();
148
+ const tooltip = document.createElement('div');
149
+
150
+ tooltip.id = id;
151
+ tooltip.className = className ? `${TOOLTIP_BASE_CLASS} ${className}` : TOOLTIP_BASE_CLASS;
152
+
153
+ tooltip.setAttribute('role', 'tooltip');
154
+ tooltip.setAttribute('aria-live', 'polite');
155
+
156
+ tooltip.textContent = text;
157
+
158
+ return tooltip;
159
+ }
160
+
161
+ function attachTooltip(tooltip: HTMLElement, reference: HTMLElement) {
162
+ const container = document.querySelector(PAGE_CONTAINER_SELECTOR) || document.body;
163
+ const ariaLive = reference.getAttribute('aria-live');
164
+
165
+ reference.setAttribute(TOOLTIP_DATA_ATTR, tooltip.id);
166
+
167
+ if (ariaLive) {
168
+ tooltip.setAttribute('aria-live', ariaLive);
169
+ }
170
+
171
+ container.appendChild(tooltip);
172
+ }
173
+
174
+ function detachTooltip(tooltip: HTMLElement) {
175
+ if (tooltip.id) {
176
+ const reference = getReferenceByTooltipId(tooltip.id);
177
+
178
+ reference?.removeAttribute(TOOLTIP_DATA_ATTR);
179
+ }
180
+
181
+ tooltip.remove();
182
+ }
183
+
184
+ function getReferenceByTooltipId(id: string) {
185
+ return document.querySelector<HTMLElement>(`[${TOOLTIP_DATA_ATTR}="${id}"]`);
186
+ }
187
+
188
+ function subscribeToScroll(reference: HTMLElement, update: () => void) {
189
+ const scrollableElement = getParentScrollableElement(reference);
190
+
191
+ scrollableElement.addEventListener('scroll', update);
192
+
193
+ return () => {
194
+ scrollableElement.removeEventListener('scroll', update);
195
+ };
196
+ }
197
+
198
+ function getParentScrollableElement(target: HTMLElement) {
199
+ const closestScrollableParent = target.closest('table') || target.closest('code');
200
+
201
+ return closestScrollableParent || target.parentElement || document.body;
202
+ }
203
+
204
+ function createTooltipContext(
205
+ referenceElement: HTMLElement,
206
+ tooltipElement: HTMLElement,
207
+ ): TooltipContext | null {
208
+ const tooltipParent = tooltipElement.parentElement;
209
+
210
+ if (!tooltipParent) {
211
+ return null;
212
+ }
213
+
214
+ const elements: TooltipContextElements = {
215
+ reference: referenceElement,
216
+ tooltip: tooltipElement,
217
+ offsetParent: tooltipParent,
218
+ };
219
+
220
+ const viewport = getViewportRect();
221
+ const reference = getElementRect(referenceElement);
222
+ const {width, height} = tooltipElement.getBoundingClientRect();
223
+
224
+ return {
225
+ isRtl: document.dir === 'rtl',
226
+ viewport,
227
+ reference,
228
+ tooltip: createRect({top: 0, left: 0, width, height}),
229
+ elements,
230
+ };
231
+ }
232
+
233
+ function updateTooltipPosition(
234
+ options: TooltipOptions,
235
+ referenceElement: HTMLElement,
236
+ tooltipElement: HTMLElement,
237
+ ) {
238
+ const context = createTooltipContext(referenceElement, tooltipElement);
239
+
240
+ if (!context) {
241
+ return;
242
+ }
243
+
244
+ const coords = getTooltipCoords(context, options);
245
+
246
+ tooltipElement.style.top = `${coords.y}px`;
247
+ tooltipElement.style.left = `${coords.x}px`;
248
+ }
249
+
250
+ function getTooltipCoords(context: TooltipContext, options: TooltipOptions): Coords {
251
+ const {placement = 'bottom-start', offset = DEFAULT_OFFSET_VALUES, flip = true} = options;
252
+ const {reference, tooltip, viewport, isRtl} = context;
253
+
254
+ const {coords} = computePosition(reference, tooltip, viewport, placement, offset, isRtl, flip);
255
+
256
+ const rect = updateRect(tooltip, {top: coords.y, left: coords.x});
257
+ const relativeRect = convertToRelativeToOffsetParentRect(rect, context.elements.offsetParent);
258
+
259
+ return {
260
+ x: relativeRect.left,
261
+ y: relativeRect.top,
262
+ };
263
+ }
@@ -0,0 +1,59 @@
1
+ type Prettify<T> = {
2
+ [K in keyof T]: T[K];
3
+ } & {};
4
+
5
+ export interface ElementRect {
6
+ readonly top: number;
7
+ readonly left: number;
8
+ readonly right: number;
9
+ readonly bottom: number;
10
+ readonly width: number;
11
+ readonly height: number;
12
+ }
13
+
14
+ export interface Coords {
15
+ x: number;
16
+ y: number;
17
+ }
18
+
19
+ export interface OffsetValues {
20
+ mainAxis?: number;
21
+ crossAxis?: number;
22
+ }
23
+
24
+ export type Alignment = 'start' | 'end';
25
+
26
+ export type Side = 'top' | 'right' | 'bottom' | 'left';
27
+
28
+ export type AlignedPlacement = `${Side}-${Alignment}`;
29
+
30
+ export type Placement = Prettify<Side | AlignedPlacement>;
31
+
32
+ export type SideObject = {[key in Side]: number};
33
+
34
+ export interface TooltipContextElements {
35
+ reference: HTMLElement;
36
+ tooltip: HTMLElement;
37
+ offsetParent: HTMLElement;
38
+ }
39
+
40
+ export interface TooltipContext {
41
+ isRtl: boolean;
42
+ viewport: ElementRect;
43
+ reference: ElementRect;
44
+ tooltip: ElementRect;
45
+ elements: TooltipContextElements;
46
+ }
47
+
48
+ export interface TooltipElementOptions {
49
+ text: string;
50
+ className?: string;
51
+ }
52
+
53
+ export interface TooltipOptions {
54
+ closeDelay?: number;
55
+ additionalClassName?: string;
56
+ placement?: Placement;
57
+ offset?: OffsetValues;
58
+ flip?: boolean;
59
+ }