lakelib 0.3.8 → 0.3.10

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/lib/lake.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import EventEmitter from 'eventemitter3';
2
2
  import { i18nObject as i18nObject$1 } from 'typesafe-i18n';
3
+ import { isKeyHotkey } from 'is-hotkey';
3
4
  import debounce from 'debounce';
4
5
  import isEqual from 'fast-deep-equal/es6';
5
- import { isKeyHotkey } from 'is-hotkey';
6
6
  import 'photoswipe/style.css';
7
7
  import PhotoSwipeLightbox from 'photoswipe/lightbox';
8
8
  import PhotoSwipe from 'photoswipe';
@@ -149,6 +149,8 @@ var specialCharacter$1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\"
149
149
 
150
150
  var equation$1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" fill=\"#000000\" viewBox=\"0 0 256 256\"><path d=\"M184,72V56H80.65l53.6,67a8,8,0,0,1,0,10l-53.6,67H184V184a8,8,0,0,1,16,0v24a8,8,0,0,1-8,8H64a8,8,0,0,1-6.25-13l60-75-60-75A8,8,0,0,1,64,40H192a8,8,0,0,1,8,8V72a8,8,0,0,1-16,0Z\"></path></svg>";
151
151
 
152
+ var twitter$1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" fill=\"#000000\" viewBox=\"0 0 256 256\"><path d=\"M214.75,211.71l-62.6-98.38,61.77-67.95a8,8,0,0,0-11.84-10.76L143.24,99.34,102.75,35.71A8,8,0,0,0,96,32H48a8,8,0,0,0-6.75,12.3l62.6,98.37-61.77,68a8,8,0,1,0,11.84,10.76l58.84-64.72,40.49,63.63A8,8,0,0,0,160,224h48a8,8,0,0,0,6.75-12.29ZM164.39,208,62.57,48h29L193.43,208Z\"></path></svg>";
153
+
152
154
  var table$1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" fill=\"#000000\" viewBox=\"0 0 256 256\"><path d=\"M216,48H40A16,16,0,0,0,24,64V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM104,144V112h48v32Zm48,16v32H104V160ZM40,112H88v32H40Zm64-16V64h48V96Zm64,16h48v32H168Zm48-16H168V64h48ZM88,64V96H40V64ZM40,160H88v32H40Zm176,32H168V160h48v32Z\"></path></svg>";
153
155
 
154
156
  var unlink = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" fill=\"#000000\" viewBox=\"0 0 256 256\"><path d=\"M190.63,65.37a32,32,0,0,0-45.19-.06L133.79,77.52a8,8,0,0,1-11.58-11l11.72-12.29a1.59,1.59,0,0,1,.13-.13,48,48,0,0,1,67.88,67.88,1.59,1.59,0,0,1-.13.13l-12.29,11.72a8,8,0,0,1-11-11.58l12.21-11.65A32,32,0,0,0,190.63,65.37ZM122.21,178.48l-11.65,12.21a32,32,0,0,1-45.25-45.25l12.21-11.65a8,8,0,0,0-11-11.58L54.19,133.93a1.59,1.59,0,0,0-.13.13,48,48,0,0,0,67.88,67.88,1.59,1.59,0,0,0,.13-.13l11.72-12.29a8,8,0,1,0-11.58-11ZM208,152H184a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16ZM48,104H72a8,8,0,0,0,0-16H48a8,8,0,0,0,0,16Zm112,72a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V184A8,8,0,0,0,160,176ZM96,80a8,8,0,0,0,8-8V48a8,8,0,0,0-16,0V72A8,8,0,0,0,96,80Z\"></path></svg>";
@@ -263,6 +265,7 @@ const icons = new Map([
263
265
  ['emoji', emoji$1],
264
266
  ['specialCharacter', specialCharacter$1],
265
267
  ['equation', equation$1],
268
+ ['twitter', twitter$1],
266
269
  ['table', table$1],
267
270
  // link
268
271
  ['unlink', unlink],
@@ -1649,7 +1652,7 @@ class Range {
1649
1652
  return this.range.endOffset;
1650
1653
  }
1651
1654
  /**
1652
- * The deepest or furthest down the document tree — node that contains both boundary points of the range.
1655
+ * The deepest node, or the lowest point in the document tree, that contains both boundary points of the range.
1653
1656
  */
1654
1657
  get commonAncestor() {
1655
1658
  return new Nodes(this.range.commonAncestorContainer);
@@ -2864,6 +2867,15 @@ const toolbarItems = [
2864
2867
  editor.command.execute(value);
2865
2868
  },
2866
2869
  },
2870
+ {
2871
+ name: 'twitter',
2872
+ type: 'button',
2873
+ icon: icons.get('twitter'),
2874
+ tooltip: locale => locale.toolbar.twitter(),
2875
+ onClick: (editor, value) => {
2876
+ editor.command.execute(value);
2877
+ },
2878
+ },
2867
2879
  {
2868
2880
  name: 'heading',
2869
2881
  type: 'dropdown',
@@ -3318,7 +3330,7 @@ var enUS = {
3318
3330
  formatPainter: 'Format painter',
3319
3331
  link: 'Link',
3320
3332
  hr: 'Horizontal line',
3321
- video: 'Video',
3333
+ video: 'YouTube',
3322
3334
  codeBlock: 'Code block',
3323
3335
  heading: 'Heading',
3324
3336
  heading1: 'Heading 1',
@@ -3340,6 +3352,7 @@ var enUS = {
3340
3352
  file: 'File',
3341
3353
  emoji: 'Emoji',
3342
3354
  equation: 'Mathematical formula',
3355
+ twitter: 'X (Tweet)',
3343
3356
  removeColor: 'Remove color',
3344
3357
  },
3345
3358
  slash: {
@@ -3379,10 +3392,12 @@ var enUS = {
3379
3392
  hrDesc: 'Insert a horizontal line',
3380
3393
  codeBlock: 'Code block',
3381
3394
  codeBlockDesc: 'Insert a code block',
3382
- video: 'Video',
3383
- videoDesc: 'Insert a video from YouTube',
3395
+ video: 'YouTube',
3396
+ videoDesc: 'Insert a YouTube video',
3384
3397
  equation: 'Mathematical formula',
3385
3398
  equationDesc: 'Insert a TeX expression',
3399
+ twitter: 'Tweet',
3400
+ twitterDesc: 'Insert an X (Tweet)',
3386
3401
  image: 'Image',
3387
3402
  imageDesc: 'Upload an image',
3388
3403
  file: 'File',
@@ -3446,7 +3461,7 @@ var enUS = {
3446
3461
  video: {
3447
3462
  embed: 'Embed video',
3448
3463
  remove: 'Delete',
3449
- description: 'Paste a link to embed a video from YouTube.',
3464
+ description: 'Paste a YouTube link to embed the video.',
3450
3465
  url: 'Link',
3451
3466
  urlError: 'Please enter a valid link.',
3452
3467
  },
@@ -3458,6 +3473,13 @@ var enUS = {
3458
3473
  help: 'Supported functions',
3459
3474
  placeholder: 'Type a TeX expression...',
3460
3475
  },
3476
+ twitter: {
3477
+ embed: 'Embed Tweet',
3478
+ remove: 'Delete',
3479
+ description: 'Paste an X (Twitter) link to embed the post.',
3480
+ url: 'Link',
3481
+ urlError: 'Please enter a valid link.',
3482
+ },
3461
3483
  };
3462
3484
 
3463
3485
  var zhCN = {
@@ -3487,7 +3509,7 @@ var zhCN = {
3487
3509
  formatPainter: '格式刷',
3488
3510
  link: '链接',
3489
3511
  hr: '分割线',
3490
- video: '视频',
3512
+ video: 'YouTube',
3491
3513
  codeBlock: '代码块',
3492
3514
  heading: '标题',
3493
3515
  heading1: '标题 1',
@@ -3509,6 +3531,7 @@ var zhCN = {
3509
3531
  file: '文件',
3510
3532
  emoji: '表情',
3511
3533
  equation: '数学公式',
3534
+ twitter: 'X (Tweet)',
3512
3535
  removeColor: '默认',
3513
3536
  },
3514
3537
  slash: {
@@ -3548,10 +3571,12 @@ var zhCN = {
3548
3571
  hrDesc: '插入分割线',
3549
3572
  codeBlock: '代码块',
3550
3573
  codeBlockDesc: '插入代码块',
3551
- video: '视频',
3574
+ video: 'YouTube',
3552
3575
  videoDesc: '插入 YouTube 视频',
3553
3576
  equation: '数学公式',
3554
3577
  equationDesc: '支持 TeX 语法',
3578
+ twitter: 'Tweet',
3579
+ twitterDesc: '插入 Tweet',
3555
3580
  image: '图片',
3556
3581
  imageDesc: '上传图片',
3557
3582
  file: '文件',
@@ -3627,6 +3652,13 @@ var zhCN = {
3627
3652
  help: '支持的功能',
3628
3653
  placeholder: '请输入 TeX 表达式',
3629
3654
  },
3655
+ twitter: {
3656
+ embed: '嵌入 Tweet',
3657
+ remove: '删除',
3658
+ description: '在下面的输入框里,粘贴 X (Twitter) 链接。',
3659
+ url: '链接',
3660
+ urlError: '请输入有效的链接。',
3661
+ },
3630
3662
  };
3631
3663
 
3632
3664
  var ja = {
@@ -3656,7 +3688,7 @@ var ja = {
3656
3688
  formatPainter: '形式ペインタ',
3657
3689
  link: 'リンク',
3658
3690
  hr: '区切り線',
3659
- video: '動画',
3691
+ video: 'YouTube',
3660
3692
  codeBlock: 'コードブロック',
3661
3693
  heading: '見出し',
3662
3694
  heading1: '見出し 1',
@@ -3678,6 +3710,7 @@ var ja = {
3678
3710
  file: 'ファイル',
3679
3711
  emoji: '絵文字',
3680
3712
  equation: '数式',
3713
+ twitter: 'ツイート',
3681
3714
  removeColor: 'デフォルト',
3682
3715
  },
3683
3716
  slash: {
@@ -3717,10 +3750,12 @@ var ja = {
3717
3750
  hrDesc: '水平線を挿入',
3718
3751
  codeBlock: 'コードブロック',
3719
3752
  codeBlockDesc: 'コードブロックを挿入',
3720
- video: '動画',
3753
+ video: 'YouTube',
3721
3754
  videoDesc: 'YouTube から動画を挿入',
3722
3755
  equation: '数式',
3723
3756
  equationDesc: 'TeX 数式を挿入',
3757
+ twitter: 'ツイート',
3758
+ twitterDesc: 'ツイートを挿入',
3724
3759
  image: '画像',
3725
3760
  imageDesc: '画像をアップロード',
3726
3761
  file: 'ファイル',
@@ -3796,6 +3831,13 @@ var ja = {
3796
3831
  help: 'サポートされている機能',
3797
3832
  placeholder: 'TeX 数式を入力してください',
3798
3833
  },
3834
+ twitter: {
3835
+ embed: 'ツイートを埋め込む',
3836
+ remove: '削除',
3837
+ description: '下の入力欄に X (Twitter) リンクを貼り付けてください。',
3838
+ url: 'リンク',
3839
+ urlError: '有効なリンクを入力してください。',
3840
+ },
3799
3841
  };
3800
3842
 
3801
3843
  var ko = {
@@ -3825,7 +3867,7 @@ var ko = {
3825
3867
  formatPainter: '형식 복사기',
3826
3868
  link: '링크',
3827
3869
  hr: '구분선',
3828
- video: '동영상',
3870
+ video: '유튜브',
3829
3871
  codeBlock: '코드 블록',
3830
3872
  heading: '제목',
3831
3873
  heading1: '제목 1',
@@ -3847,6 +3889,7 @@ var ko = {
3847
3889
  file: '파일',
3848
3890
  emoji: '이모지',
3849
3891
  equation: '수학 공식',
3892
+ twitter: '트윗',
3850
3893
  removeColor: '기본 색상',
3851
3894
  },
3852
3895
  slash: {
@@ -3873,7 +3916,7 @@ var ko = {
3873
3916
  checklist: '체크리스트',
3874
3917
  checklistDesc: '체크리스트를 작성',
3875
3918
  table: '표',
3876
- tableDesc: '표를 삽입',
3919
+ tableDesc: '표를 추가',
3877
3920
  infoAlert: '정보 블록',
3878
3921
  infoAlertDesc: '정보 블록을 작성',
3879
3922
  tipAlert: '팁 블록',
@@ -3883,13 +3926,15 @@ var ko = {
3883
3926
  dangerAlert: '위험 블록',
3884
3927
  dangerAlertDesc: '위험 블록을 작성',
3885
3928
  hr: '구분선',
3886
- hrDesc: '구분선을 삽입',
3929
+ hrDesc: '구분선을 추가',
3887
3930
  codeBlock: '코드 블록',
3888
- codeBlockDesc: '코드 블록을 삽입',
3889
- video: '동영상',
3890
- videoDesc: '유튜브 동영상을 삽입',
3931
+ codeBlockDesc: '코드 블록을 추가',
3932
+ video: '유튜브',
3933
+ videoDesc: '유튜브 동영상을 추가',
3891
3934
  equation: '수학 공식',
3892
- equationDesc: 'TeX 수식을 삽입',
3935
+ equationDesc: 'TeX 수식을 추가',
3936
+ twitter: '트윗',
3937
+ twitterDesc: '트윗을 추가',
3893
3938
  image: '이미지',
3894
3939
  imageDesc: '이미지를 업로드',
3895
3940
  file: '파일',
@@ -3908,12 +3953,12 @@ var ko = {
3908
3953
  fitTable: '페이지 너비에 맞게 조정',
3909
3954
  cellBackground: '셀 배경 색상',
3910
3955
  column: '열',
3911
- insertColumnLeft: '왼쪽에 열을 삽입',
3912
- insertColumnRight: '오른쪽에 열을 삽입',
3956
+ insertColumnLeft: '왼쪽에 열을 추가',
3957
+ insertColumnRight: '오른쪽에 열을 추가',
3913
3958
  deleteColumn: '열을 삭제',
3914
3959
  row: '행',
3915
- insertRowAbove: '위에 행을 삽입',
3916
- insertRowBelow: '아래에 행을 삽입',
3960
+ insertRowAbove: '위에 행을 추가',
3961
+ insertRowBelow: '아래에 행을 추가',
3917
3962
  deleteRow: '행을 삭제',
3918
3963
  merge: '셀을 병합',
3919
3964
  mergeUp: '위쪽으로 셀을 병합',
@@ -3951,9 +3996,9 @@ var ko = {
3951
3996
  remove: '삭제',
3952
3997
  },
3953
3998
  video: {
3954
- embed: '동영상 삽입',
3999
+ embed: '동영상을 추가',
3955
4000
  remove: '삭제',
3956
- description: '아래 입력란에 YouTube 링크를 붙여넣으세요.',
4001
+ description: '아래 입력란에 유튜브 링크를 붙여넣으세요.',
3957
4002
  url: '링크',
3958
4003
  urlError: '유효한 링크를 입력하세요.',
3959
4004
  },
@@ -3965,6 +4010,13 @@ var ko = {
3965
4010
  help: '지원되는 기능',
3966
4011
  placeholder: 'TeX 수식을 입력하세요',
3967
4012
  },
4013
+ twitter: {
4014
+ embed: '트윗을 추가',
4015
+ remove: '삭제',
4016
+ description: '아래 입력란에 X (Twitter) 링크를 붙여넣으세요.',
4017
+ url: '링크',
4018
+ urlError: '유효한 링크를 입력하세요.',
4019
+ },
3968
4020
  };
3969
4021
 
3970
4022
  const localeTranslations = {
@@ -4687,9 +4739,7 @@ class Box {
4687
4739
  if (component === undefined) {
4688
4740
  throw new Error(`Box "${node}" has not been defined yet.`);
4689
4741
  }
4690
- const type = encode(component.type);
4691
- const name = encode(component.name);
4692
- this.node = query(template `<lake-box type="${type}" name="${name}"></lake-box>`);
4742
+ this.node = query(template `<lake-box type="${component.type}" name="${component.name}"></lake-box>`);
4693
4743
  if (component.value) {
4694
4744
  this.value = component.value;
4695
4745
  }
@@ -4906,6 +4956,281 @@ function getBox(boxNode) {
4906
4956
  return box;
4907
4957
  }
4908
4958
 
4959
+ // The CornerToolbar class represents a button group located in the top-right corner of a box.
4960
+ class CornerToolbar {
4961
+ constructor(config) {
4962
+ this.config = config;
4963
+ this.locale = this.config.locale || i18nObject('en-US');
4964
+ this.root = query(config.root);
4965
+ this.container = query('<div class="lake-corner-toolbar" />');
4966
+ }
4967
+ appendButton(item) {
4968
+ const buttonNode = query(template `
4969
+ <button type="button" name="${item.name}" tabindex="-1" />
4970
+ `);
4971
+ const tooltip = typeof item.tooltip === 'string' ? item.tooltip : item.tooltip(this.locale);
4972
+ buttonNode.attr('title', tooltip);
4973
+ if (item.icon) {
4974
+ buttonNode.append(item.icon);
4975
+ }
4976
+ this.container.append(buttonNode);
4977
+ buttonNode.on('click', event => {
4978
+ event.preventDefault();
4979
+ item.onClick(event);
4980
+ });
4981
+ }
4982
+ render() {
4983
+ const { items } = this.config;
4984
+ if (items.length === 0) {
4985
+ return;
4986
+ }
4987
+ this.root.append(this.container);
4988
+ for (const item of items) {
4989
+ this.appendButton(item);
4990
+ }
4991
+ }
4992
+ }
4993
+
4994
+ // The Resizer class represents a UI component used to resize images or videos.
4995
+ class Resizer {
4996
+ constructor(config) {
4997
+ this.config = config;
4998
+ this.root = query(config.root);
4999
+ this.target = query(config.target);
5000
+ this.container = query(template `
5001
+ <div class="lake-resizer">
5002
+ <div class="lake-resizer-top-left"></div>
5003
+ <div class="lake-resizer-top-right"></div>
5004
+ <div class="lake-resizer-bottom-left"></div>
5005
+ <div class="lake-resizer-bottom-right"></div>
5006
+ <div class="lake-resizer-info"></div>
5007
+ </div>
5008
+ `);
5009
+ }
5010
+ bindEvents(pointerNode) {
5011
+ const target = this.target;
5012
+ const infoNode = this.container.find('.lake-resizer-info');
5013
+ const isPlus = pointerNode.attr('class').indexOf('-right') >= 0;
5014
+ const initialWidth = target.width();
5015
+ const initialHeight = target.height();
5016
+ const rate = initialHeight / initialWidth;
5017
+ let clientX = 0;
5018
+ let width = 0;
5019
+ // resizing box
5020
+ const pointermoveListener = (event) => {
5021
+ const pointerEvent = event;
5022
+ const diffX = pointerEvent.clientX - clientX;
5023
+ const newWidth = Math.round(isPlus ? width + diffX : width - diffX);
5024
+ const newHeight = Math.round(rate * newWidth);
5025
+ infoNode.text(`${newWidth} x ${newHeight}`);
5026
+ target.css({
5027
+ width: `${newWidth}px`,
5028
+ height: `${newHeight}px`,
5029
+ });
5030
+ if (this.config.onResize) {
5031
+ this.config.onResize(newWidth, newHeight);
5032
+ }
5033
+ };
5034
+ // start resizing
5035
+ const pointerdownListener = (event) => {
5036
+ const pointerEvent = event;
5037
+ const pointerNativeNode = pointerNode.get(0);
5038
+ // The capture will be implicitly released after a pointerup or pointercancel event.
5039
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
5040
+ try {
5041
+ // Test case throws an exception on Firefox.
5042
+ pointerNativeNode.setPointerCapture(pointerEvent.pointerId);
5043
+ }
5044
+ catch ( /* empty */_a) { /* empty */ }
5045
+ clientX = pointerEvent.clientX;
5046
+ width = target.width();
5047
+ infoNode.text(`${initialWidth} x ${initialHeight}`);
5048
+ infoNode.show();
5049
+ pointerNode.on('pointermove', pointermoveListener);
5050
+ };
5051
+ // stop resizing
5052
+ const pointerupListner = () => {
5053
+ pointerNode.off('pointermove');
5054
+ infoNode.hide();
5055
+ width = target.width();
5056
+ const height = Math.round(rate * width);
5057
+ this.config.onStop(width, height);
5058
+ };
5059
+ // cancel resizing
5060
+ const pointercancelListner = () => {
5061
+ pointerNode.off('pointermove');
5062
+ infoNode.hide();
5063
+ };
5064
+ pointerNode.on('pointerdown', pointerdownListener);
5065
+ pointerNode.on('pointerup', pointerupListner);
5066
+ pointerNode.on('pointercancel', pointercancelListner);
5067
+ }
5068
+ render() {
5069
+ this.bindEvents(this.container.find('.lake-resizer-top-left'));
5070
+ this.bindEvents(this.container.find('.lake-resizer-top-right'));
5071
+ this.bindEvents(this.container.find('.lake-resizer-bottom-left'));
5072
+ this.bindEvents(this.container.find('.lake-resizer-bottom-right'));
5073
+ this.root.append(this.container);
5074
+ }
5075
+ }
5076
+
5077
+ /**
5078
+ * Returns the localized string.
5079
+ */
5080
+ function getLocaleString(locale, value) {
5081
+ return typeof value === 'string' ? value : value(locale);
5082
+ }
5083
+ /**
5084
+ * Appends a corner toolbar to the iframe box.
5085
+ */
5086
+ function appendCornerToolbar(config, box) {
5087
+ const editor = box.getEditor();
5088
+ const boxContainer = box.getContainer();
5089
+ const rootNode = boxContainer.find('.lake-iframe');
5090
+ if (rootNode.find('.lake-corner-toolbar').length > 0) {
5091
+ return;
5092
+ }
5093
+ new CornerToolbar({
5094
+ locale: editor.locale,
5095
+ root: rootNode,
5096
+ items: [
5097
+ {
5098
+ name: 'remove',
5099
+ icon: icons.get('remove'),
5100
+ tooltip: config.deleteButtonText,
5101
+ onClick: event => {
5102
+ event.stopPropagation();
5103
+ editor.selection.removeBox(box);
5104
+ editor.history.save();
5105
+ },
5106
+ },
5107
+ ],
5108
+ }).render();
5109
+ }
5110
+ /**
5111
+ * Shows the iframe in the box.
5112
+ */
5113
+ function showIframe(config, box) {
5114
+ const editor = box.getEditor();
5115
+ const boxContainer = box.getContainer();
5116
+ const value = box.value;
5117
+ const width = value.width || config.width;
5118
+ const height = value.height || config.height;
5119
+ const iframeNode = query('<iframe></iframe>');
5120
+ iframeNode.css({
5121
+ width,
5122
+ height,
5123
+ });
5124
+ const iframeAttributes = config.iframeAttributes(value.url);
5125
+ for (const key of Object.keys(iframeAttributes)) {
5126
+ iframeNode.attr(key, iframeAttributes[key]);
5127
+ }
5128
+ const placeholderNode = query('<div class="lake-iframe-placeholder" />');
5129
+ placeholderNode.css({
5130
+ width,
5131
+ height,
5132
+ });
5133
+ if (config.iframePlaceholder) {
5134
+ placeholderNode.append(config.iframePlaceholder);
5135
+ }
5136
+ const rootNode = boxContainer.find('.lake-iframe');
5137
+ iframeNode.on('load', () => {
5138
+ placeholderNode.remove();
5139
+ if (editor.readonly) {
5140
+ return;
5141
+ }
5142
+ if (config.resize === true && rootNode.find('.lake-resizer').length === 0) {
5143
+ new Resizer({
5144
+ root: rootNode,
5145
+ target: iframeNode,
5146
+ onStop: (newWidth, newHeight) => {
5147
+ box.updateValue({
5148
+ width: `${newWidth}px`,
5149
+ height: `${newHeight}px`,
5150
+ });
5151
+ editor.selection.selectBox(box);
5152
+ editor.history.save();
5153
+ },
5154
+ }).render();
5155
+ }
5156
+ });
5157
+ if (config.validUrl(value.url)) {
5158
+ rootNode.prepend(iframeNode);
5159
+ }
5160
+ else {
5161
+ placeholderNode.css('position', 'static');
5162
+ }
5163
+ rootNode.prepend(placeholderNode);
5164
+ if (config.beforeIframeLoad) {
5165
+ config.beforeIframeLoad(box);
5166
+ }
5167
+ }
5168
+ /**
5169
+ * Creates an iframe box component with configurable properties.
5170
+ * This component supports rendering an iframe with customizable attributes, resizing, and toolbar functionalities.
5171
+ */
5172
+ function createIframeBox(config) {
5173
+ return {
5174
+ type: config.type,
5175
+ name: config.name,
5176
+ render: box => {
5177
+ const editor = box.getEditor();
5178
+ const locale = editor.locale;
5179
+ const value = box.value;
5180
+ const boxContainer = box.getContainer();
5181
+ const rootNode = query('<div class="lake-iframe" />');
5182
+ boxContainer.empty();
5183
+ boxContainer.append(rootNode);
5184
+ if (value.url === undefined) {
5185
+ if (editor.readonly) {
5186
+ box.node.hide();
5187
+ return;
5188
+ }
5189
+ const formNode = query(template `
5190
+ <div class="lake-iframe-form">
5191
+ <div class="lake-description">${getLocaleString(locale, config.formDescription)}</div>
5192
+ <div class="lake-input-label">${getLocaleString(locale, config.urlLabel || '')}</div>
5193
+ <div class="lake-input-field">
5194
+ <input type="text" name="url" placeholder="${config.urlPlaceholder}" />
5195
+ </div>
5196
+ </div>
5197
+ `);
5198
+ const button = new Button({
5199
+ root: formNode.find('.lake-input-field'),
5200
+ name: 'embed',
5201
+ type: 'primary',
5202
+ text: getLocaleString(locale, config.embedButtonText),
5203
+ onClick: () => {
5204
+ const url = formNode.find('input[name="url"]').value();
5205
+ if (!config.validUrl(url)) {
5206
+ editor.config.onMessage('error', getLocaleString(locale, config.urlError));
5207
+ return;
5208
+ }
5209
+ box.updateValue('url', url);
5210
+ editor.history.save();
5211
+ formNode.remove();
5212
+ showIframe(config, box);
5213
+ },
5214
+ });
5215
+ formNode.find('input[name="url"]').on('keydown', event => {
5216
+ if (isKeyHotkey('enter', event)) {
5217
+ event.preventDefault();
5218
+ button.node.emit('click');
5219
+ }
5220
+ });
5221
+ button.render();
5222
+ rootNode.append(formNode);
5223
+ }
5224
+ else {
5225
+ showIframe(config, box);
5226
+ }
5227
+ if (!editor.readonly) {
5228
+ appendCornerToolbar(config, box);
5229
+ }
5230
+ },
5231
+ };
5232
+ }
5233
+
4909
5234
  /**
4910
5235
  * The Fragment interface represents a lightweight document object that has no parent.
4911
5236
  * It is designed for efficient manipulation of document structures without affecting the main DOM.
@@ -6273,7 +6598,7 @@ function removeBox(range) {
6273
6598
  return box;
6274
6599
  }
6275
6600
 
6276
- var version = "0.3.8";
6601
+ var version = "0.3.10";
6277
6602
 
6278
6603
  // Converts the custom HTML tags to the special tags that can not be parsed by browser.
6279
6604
  function denormalizeValue(value) {
@@ -7938,6 +8263,28 @@ class Editor {
7938
8263
  }
7939
8264
  });
7940
8265
  }
8266
+ /**
8267
+ * Binds events for pointer.
8268
+ */
8269
+ bindPointerEvents() {
8270
+ this.container.on('pointerdown', event => {
8271
+ const pointerEvent = event;
8272
+ if (pointerEvent.target !== null && pointerEvent.target !== this.container.get(0)) {
8273
+ return;
8274
+ }
8275
+ const lastChild = this.container.last();
8276
+ if (lastChild.isTable || lastChild.isBlockBox) {
8277
+ const lastChildRect = lastChild.get(0).getBoundingClientRect();
8278
+ if (pointerEvent.clientY > lastChildRect.bottom) {
8279
+ pointerEvent.preventDefault();
8280
+ const paragraph = query('<p><br /></p>');
8281
+ lastChild.after(paragraph);
8282
+ this.selection.range.shrinkBefore(paragraph);
8283
+ this.selection.sync();
8284
+ }
8285
+ }
8286
+ });
8287
+ }
7941
8288
  /**
7942
8289
  * Returns translation functions for the specified language.
7943
8290
  */
@@ -8161,6 +8508,7 @@ class Editor {
8161
8508
  document.addEventListener('click', this.clickListener);
8162
8509
  this.bindInputEvents();
8163
8510
  this.bindHistoryEvents();
8511
+ this.bindPointerEvents();
8164
8512
  }
8165
8513
  }
8166
8514
  /**
@@ -10659,124 +11007,6 @@ var codeBlock = (editor) => {
10659
11007
  });
10660
11008
  };
10661
11009
 
10662
- // The CornerToolbar class represents a button group located in the top-right corner of a box.
10663
- class CornerToolbar {
10664
- constructor(config) {
10665
- this.config = config;
10666
- this.locale = this.config.locale || i18nObject('en-US');
10667
- this.root = query(config.root);
10668
- this.container = query('<div class="lake-corner-toolbar" />');
10669
- }
10670
- appendButton(item) {
10671
- const buttonNode = query(template `
10672
- <button type="button" name="${item.name}" tabindex="-1" />
10673
- `);
10674
- const tooltip = typeof item.tooltip === 'string' ? item.tooltip : item.tooltip(this.locale);
10675
- buttonNode.attr('title', tooltip);
10676
- if (item.icon) {
10677
- buttonNode.append(item.icon);
10678
- }
10679
- this.container.append(buttonNode);
10680
- buttonNode.on('click', event => {
10681
- event.preventDefault();
10682
- item.onClick(event);
10683
- });
10684
- }
10685
- render() {
10686
- const { items } = this.config;
10687
- if (items.length === 0) {
10688
- return;
10689
- }
10690
- this.root.append(this.container);
10691
- for (const item of items) {
10692
- this.appendButton(item);
10693
- }
10694
- }
10695
- }
10696
-
10697
- // The Resizer class represents a UI component used to resize images or videos.
10698
- class Resizer {
10699
- constructor(config) {
10700
- this.config = config;
10701
- this.root = query(config.root);
10702
- this.target = query(config.target);
10703
- this.container = query(template `
10704
- <div class="lake-resizer">
10705
- <div class="lake-resizer-top-left"></div>
10706
- <div class="lake-resizer-top-right"></div>
10707
- <div class="lake-resizer-bottom-left"></div>
10708
- <div class="lake-resizer-bottom-right"></div>
10709
- <div class="lake-resizer-info"></div>
10710
- </div>
10711
- `);
10712
- }
10713
- bindEvents(pointerNode) {
10714
- const target = this.target;
10715
- const infoNode = this.container.find('.lake-resizer-info');
10716
- const isPlus = pointerNode.attr('class').indexOf('-right') >= 0;
10717
- const initialWidth = target.width();
10718
- const initialHeight = target.height();
10719
- const rate = initialHeight / initialWidth;
10720
- let clientX = 0;
10721
- let width = 0;
10722
- // resizing box
10723
- const pointermoveListener = (event) => {
10724
- const pointerEvent = event;
10725
- const diffX = pointerEvent.clientX - clientX;
10726
- const newWidth = Math.round(isPlus ? width + diffX : width - diffX);
10727
- const newHeight = Math.round(rate * newWidth);
10728
- infoNode.text(`${newWidth} x ${newHeight}`);
10729
- target.css({
10730
- width: `${newWidth}px`,
10731
- height: `${newHeight}px`,
10732
- });
10733
- if (this.config.onResize) {
10734
- this.config.onResize(newWidth, newHeight);
10735
- }
10736
- };
10737
- // start resizing
10738
- const pointerdownListener = (event) => {
10739
- const pointerEvent = event;
10740
- const pointerNativeNode = pointerNode.get(0);
10741
- // The capture will be implicitly released after a pointerup or pointercancel event.
10742
- // https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
10743
- try {
10744
- // Test case throws an exception on Firefox.
10745
- pointerNativeNode.setPointerCapture(pointerEvent.pointerId);
10746
- }
10747
- catch ( /* empty */_a) { /* empty */ }
10748
- clientX = pointerEvent.clientX;
10749
- width = target.width();
10750
- infoNode.text(`${initialWidth} x ${initialHeight}`);
10751
- infoNode.show();
10752
- pointerNode.on('pointermove', pointermoveListener);
10753
- };
10754
- // stop resizing
10755
- const pointerupListner = () => {
10756
- pointerNode.off('pointermove');
10757
- infoNode.hide();
10758
- width = target.width();
10759
- const height = Math.round(rate * width);
10760
- this.config.onStop(width, height);
10761
- };
10762
- // cancel resizing
10763
- const pointercancelListner = () => {
10764
- pointerNode.off('pointermove');
10765
- infoNode.hide();
10766
- };
10767
- pointerNode.on('pointerdown', pointerdownListener);
10768
- pointerNode.on('pointerup', pointerupListner);
10769
- pointerNode.on('pointercancel', pointercancelListner);
10770
- }
10771
- render() {
10772
- this.bindEvents(this.container.find('.lake-resizer-top-left'));
10773
- this.bindEvents(this.container.find('.lake-resizer-top-right'));
10774
- this.bindEvents(this.container.find('.lake-resizer-bottom-left'));
10775
- this.bindEvents(this.container.find('.lake-resizer-bottom-right'));
10776
- this.root.append(this.container);
10777
- }
10778
- }
10779
-
10780
11010
  const alignValueMap = {
10781
11011
  start: 'left',
10782
11012
  end: 'right',
@@ -11405,158 +11635,36 @@ var image = (editor) => {
11405
11635
  });
11406
11636
  };
11407
11637
 
11408
- function getVideoId(url) {
11409
- const result = /\w+$/.exec(url || '');
11638
+ /**
11639
+ * Extracts ID from the specified URL.
11640
+ */
11641
+ function getId$1(url) {
11642
+ const result = /[\w\-]+$/.exec(url || '');
11410
11643
  return result ? result[0] : '';
11411
11644
  }
11412
- function appendButtonGroup(box) {
11413
- const editor = box.getEditor();
11414
- const boxContainer = box.getContainer();
11415
- const rootNode = boxContainer.find('.lake-video');
11416
- new CornerToolbar({
11417
- locale: editor.locale,
11418
- root: rootNode,
11419
- items: [
11420
- {
11421
- name: 'remove',
11422
- icon: icons.get('remove'),
11423
- tooltip: editor.locale.video.remove(),
11424
- onClick: event => {
11425
- event.stopPropagation();
11426
- editor.selection.removeBox(box);
11427
- editor.history.save();
11428
- },
11429
- },
11430
- ],
11431
- }).render();
11432
- }
11433
- function showVideo(box) {
11434
- const editor = box.getEditor();
11435
- const boxContainer = box.getContainer();
11436
- const value = box.value;
11437
- const width = value.width || 560;
11438
- const height = value.height || 315;
11439
- boxContainer.css({
11440
- width: `${width}px`,
11441
- height: `${height}px`,
11442
- });
11443
- const videoId = getVideoId(value.url);
11444
- if (videoId === '') {
11445
- throw new Error(`Invalid link: ${value.url}`);
11446
- }
11447
- // YouTube URL: https://www.youtube.com/watch?v=5sMBhDv4sik
11448
- // The script for embedding YouTube:
11449
- // <iframe width="560" height="315" src="https://www.youtube.com/embed/5sMBhDv4sik" title="YouTube video player"
11450
- // frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
11451
- // referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
11452
- const iframeNode = query(template `
11453
- <iframe width="100%" height="${height}" src="https://www.youtube.com/embed/${videoId}" title="YouTube video player"
11454
- frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
11455
- referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
11456
- `);
11457
- const rootNode = boxContainer.find('.lake-video');
11458
- if (!editor.readonly) {
11459
- iframeNode.on('load', () => {
11460
- appendButtonGroup(box);
11461
- new CornerToolbar({
11462
- locale: editor.locale,
11463
- root: rootNode,
11464
- items: [
11465
- {
11466
- name: 'remove',
11467
- icon: icons.get('remove'),
11468
- tooltip: editor.locale.video.remove(),
11469
- onClick: event => {
11470
- event.stopPropagation();
11471
- editor.selection.removeBox(box);
11472
- editor.history.save();
11473
- },
11474
- },
11475
- ],
11476
- }).render();
11477
- new Resizer({
11478
- root: rootNode,
11479
- target: boxContainer,
11480
- onResize: (newWidth, newHeight) => {
11481
- iframeNode.attr({
11482
- height: newHeight.toString(),
11483
- });
11484
- },
11485
- onStop: (newWidth, newHeight) => {
11486
- box.updateValue({
11487
- width: newWidth,
11488
- height: newHeight,
11489
- });
11490
- editor.history.save();
11491
- },
11492
- }).render();
11493
- });
11494
- }
11495
- rootNode.append(iframeNode);
11496
- }
11497
- var videoBox = {
11645
+ const videoBox = createIframeBox({
11498
11646
  type: 'inline',
11499
11647
  name: 'video',
11500
- render: box => {
11501
- const editor = box.getEditor();
11502
- const locale = editor.locale;
11503
- const value = box.value;
11504
- const boxContainer = box.getContainer();
11505
- const rootNode = query('<div class="lake-video" />');
11506
- boxContainer.empty();
11507
- boxContainer.css({
11508
- width: '',
11509
- height: '',
11510
- });
11511
- boxContainer.append(rootNode);
11512
- if (!value.url) {
11513
- if (editor.readonly) {
11514
- box.node.hide();
11515
- return;
11516
- }
11517
- const formNode = query(template `
11518
- <div class="lake-video-form">
11519
- <div class="lake-row lake-desc-row">${locale.video.description()}</div>
11520
- <div class="lake-row">${locale.video.url()}</div>
11521
- <div class="lake-row">
11522
- <input type="text" name="url" placeholder="https://www.youtube.com/watch?v=..." />
11523
- </div>
11524
- <div class="lake-row lake-button-row"></div>
11525
- </div>
11526
- `);
11527
- const button = new Button({
11528
- root: formNode.find('.lake-button-row'),
11529
- name: 'embed',
11530
- type: 'primary',
11531
- text: locale.video.embed(),
11532
- onClick: () => {
11533
- const url = formNode.find('input[name="url"]').value();
11534
- if (url.indexOf('https://www.youtube.com/') < 0 || getVideoId(url) === '') {
11535
- editor.config.onMessage('error', locale.video.urlError());
11536
- return;
11537
- }
11538
- box.updateValue('url', url);
11539
- editor.history.save();
11540
- formNode.remove();
11541
- showVideo(box);
11542
- },
11543
- });
11544
- formNode.find('input[name="url"]').on('keydown', event => {
11545
- if (isKeyHotkey('enter', event)) {
11546
- event.preventDefault();
11547
- button.node.emit('click');
11548
- }
11549
- });
11550
- button.render();
11551
- rootNode.append(formNode);
11552
- appendButtonGroup(box);
11553
- }
11554
- else {
11555
- showVideo(box);
11556
- }
11557
- },
11558
- };
11559
-
11648
+ width: '560px',
11649
+ height: '315px',
11650
+ formDescription: locale => locale.video.description(),
11651
+ urlLabel: locale => locale.video.url(),
11652
+ urlPlaceholder: 'https://www.youtube.com/watch?v=...',
11653
+ embedButtonText: locale => locale.video.embed(),
11654
+ deleteButtonText: locale => locale.video.remove(),
11655
+ validUrl: url => url.indexOf('https://www.youtube.com/') === 0 && getId$1(url) !== '',
11656
+ urlError: locale => locale.video.urlError(),
11657
+ iframePlaceholder: icons.get('video'),
11658
+ iframeAttributes: url => ({
11659
+ src: `https://www.youtube.com/embed/${getId$1(url)}`,
11660
+ title: 'YouTube video player',
11661
+ frameborder: '0',
11662
+ allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share',
11663
+ referrerpolicy: 'strict-origin-when-cross-origin',
11664
+ allowfullscreen: 'true',
11665
+ }),
11666
+ resize: true,
11667
+ });
11560
11668
  var video = (editor) => {
11561
11669
  if (editor.readonly) {
11562
11670
  return;
@@ -11566,7 +11674,10 @@ var video = (editor) => {
11566
11674
  const box = editor.selection.insertBox('video', value);
11567
11675
  editor.history.save();
11568
11676
  if (box) {
11569
- box.getContainer().find('input[name="url"]').focus();
11677
+ const urlInput = box.getContainer().find('input[name="url"]');
11678
+ if (urlInput.length > 0) {
11679
+ urlInput.focus();
11680
+ }
11570
11681
  }
11571
11682
  },
11572
11683
  });
@@ -12287,6 +12398,81 @@ var mention = (editor) => {
12287
12398
  };
12288
12399
  };
12289
12400
 
12401
+ /**
12402
+ * Extracts ID from the specified URL.
12403
+ */
12404
+ function getId(url) {
12405
+ const result = /\d+$/.exec(url || '');
12406
+ return result ? result[0] : '';
12407
+ }
12408
+ /**
12409
+ * Returns the current theme.
12410
+ */
12411
+ function getTheme() {
12412
+ return document.documentElement.classList.contains('lake-dark') ? 'dark' : 'light';
12413
+ }
12414
+ const twitterBox = createIframeBox({
12415
+ type: 'inline',
12416
+ name: 'twitter',
12417
+ width: '550px',
12418
+ height: '300px',
12419
+ formDescription: locale => locale.twitter.description(),
12420
+ urlLabel: locale => locale.twitter.url(),
12421
+ urlPlaceholder: 'https://x.com/username/status/...',
12422
+ embedButtonText: locale => locale.twitter.embed(),
12423
+ deleteButtonText: locale => locale.twitter.remove(),
12424
+ validUrl: url => (url.indexOf('https://x.com/') === 0 || url.indexOf('https://twitter.com/') === 0) && getId(url) !== '',
12425
+ urlError: locale => locale.twitter.urlError(),
12426
+ iframePlaceholder: icons.get('twitter'),
12427
+ iframeAttributes: url => {
12428
+ return {
12429
+ src: `https://platform.twitter.com/embed/Tweet.html?id=${getId(url)}&theme=${getTheme()}`,
12430
+ title: 'Twitter tweet',
12431
+ scrolling: 'no',
12432
+ frameborder: '0',
12433
+ allowtransparency: 'true',
12434
+ allowfullscreen: 'true',
12435
+ };
12436
+ },
12437
+ beforeIframeLoad: box => {
12438
+ const boxContainer = box.getContainer();
12439
+ const placeholder = boxContainer.find('.lake-iframe-placeholder');
12440
+ const iframe = boxContainer.find('iframe');
12441
+ if (getTheme() === 'dark') {
12442
+ iframe.css('border-radius', '13px');
12443
+ }
12444
+ const messageListener = (event) => {
12445
+ if (event.origin === 'https://platform.twitter.com') {
12446
+ const params = event.data['twttr.embed'].params;
12447
+ const height = params[0].height;
12448
+ if (height > 0) {
12449
+ placeholder.css('height', `${height}px`);
12450
+ iframe.css('height', `${height}px`);
12451
+ window.removeEventListener('message', messageListener);
12452
+ }
12453
+ }
12454
+ };
12455
+ window.addEventListener('message', messageListener);
12456
+ },
12457
+ });
12458
+ var twitter = (editor) => {
12459
+ if (editor.readonly) {
12460
+ return;
12461
+ }
12462
+ editor.command.add('twitter', {
12463
+ execute: (value) => {
12464
+ const box = editor.selection.insertBox('twitter', value);
12465
+ editor.history.save();
12466
+ if (box) {
12467
+ const urlInput = box.getContainer().find('input[name="url"]');
12468
+ if (urlInput.length > 0) {
12469
+ urlInput.focus();
12470
+ }
12471
+ }
12472
+ },
12473
+ });
12474
+ };
12475
+
12290
12476
  const headingTypeMap = new Map([
12291
12477
  ['#', 'h1'],
12292
12478
  ['##', 'h2'],
@@ -13479,6 +13665,16 @@ const slashItems = [
13479
13665
  editor.command.execute(value);
13480
13666
  },
13481
13667
  },
13668
+ {
13669
+ name: 'twitter',
13670
+ type: 'button',
13671
+ icon: icons.get('twitter'),
13672
+ title: locale => locale.slash.twitter(),
13673
+ description: locale => locale.slash.twitterDesc(),
13674
+ onClick: (editor, value) => {
13675
+ editor.command.execute(value);
13676
+ },
13677
+ },
13482
13678
  {
13483
13679
  name: 'image',
13484
13680
  type: 'upload',
@@ -13726,6 +13922,7 @@ Editor.box.add(imageBox);
13726
13922
  Editor.box.add(videoBox);
13727
13923
  Editor.box.add(fileBox);
13728
13924
  Editor.box.add(mentionBox);
13925
+ Editor.box.add(twitterBox);
13729
13926
  Editor.plugin.add('copy', copy);
13730
13927
  Editor.plugin.add('cut', cut);
13731
13928
  Editor.plugin.add('paste', paste);
@@ -13762,6 +13959,7 @@ Editor.plugin.add('emoji', emoji);
13762
13959
  Editor.plugin.add('equation', equation);
13763
13960
  Editor.plugin.add('specialCharacter', specialCharacter);
13764
13961
  Editor.plugin.add('mention', mention);
13962
+ Editor.plugin.add('twitter', twitter);
13765
13963
  Editor.plugin.add('markdown', markdown);
13766
13964
  Editor.plugin.add('enterKey', enterKey);
13767
13965
  Editor.plugin.add('shiftEnterKey', shiftEnterKey);
@@ -13772,5 +13970,5 @@ Editor.plugin.add('arrowKeys', arrowKeys);
13772
13970
  Editor.plugin.add('escapeKey', escapeKey);
13773
13971
  Editor.plugin.add('slash', slash);
13774
13972
 
13775
- export { Box, Button, Dropdown, Editor, Fragment, HTMLParser, Nodes, Range, TextParser, Toolbar, addMark, deleteContents, getBox, getContentRules, icons, insertBlock, insertBookmark, insertBox, insertContents, query, removeBox, removeMark, setBlocks, splitBlock$1 as splitBlock, splitMarks, template, toBookmark, toHex };
13973
+ export { Box, Button, Dropdown, Editor, Fragment, HTMLParser, Nodes, Range, TextParser, Toolbar, addMark, createIframeBox, deleteContents, getBox, getContentRules, icons, insertBlock, insertBookmark, insertBox, insertContents, query, removeBox, removeMark, setBlocks, splitBlock$1 as splitBlock, splitMarks, template, toBookmark, toHex };
13776
13974
  //# sourceMappingURL=lake.js.map