@fastkit/vui 0.7.56 → 0.7.61

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/dist/vui.cjs.js CHANGED
@@ -15,6 +15,9 @@ var vueAppLayout = require('@fastkit/vue-app-layout');
15
15
  var vueFormControl = require('@fastkit/vue-form-control');
16
16
  var vue3 = require('@tiptap/vue-3');
17
17
  var StarterKit = require('@tiptap/starter-kit');
18
+ var core = require('@tiptap/core');
19
+ var prosemirrorView = require('prosemirror-view');
20
+ var prosemirrorState = require('prosemirror-state');
18
21
  var extensionBulletList = require('@tiptap/extension-bullet-list');
19
22
  var extensionBold = require('@tiptap/extension-bold');
20
23
  var extensionItalic = require('@tiptap/extension-italic');
@@ -2874,13 +2877,63 @@ const VTextarea = vue.defineComponent({
2874
2877
 
2875
2878
  });
2876
2879
 
2880
+ const EDITOR_EVENTS = [
2881
+ 'beforeCreate',
2882
+ 'create',
2883
+ 'update',
2884
+ 'selectionUpdate',
2885
+ 'transaction',
2886
+ 'focus',
2887
+ 'blur',
2888
+ 'destroy',
2889
+ ];
2890
+ const prefixedEventName = (source) => {
2891
+ return `on${source.charAt(0).toUpperCase()}${source.slice(1)}`;
2892
+ };
2893
+ class WysiwygEditorInitializeContext {
2894
+ listeners = {};
2895
+ constructor(opts = {}) {
2896
+ EDITOR_EVENTS.forEach((event) => {
2897
+ this.listeners[event] = [];
2898
+ const prefixed = prefixedEventName(event);
2899
+ const fn = opts[prefixed];
2900
+ fn && this.listeners[event].push(fn);
2901
+ });
2902
+ }
2903
+ on(ev, handler) {
2904
+ this.listeners[ev].push(handler);
2905
+ return () => this.off(ev, handler);
2906
+ }
2907
+ off(ev, handler) {
2908
+ this.listeners[ev] = this.listeners[ev].filter((_handler) => _handler !== handler);
2909
+ }
2910
+ editorOptions() {
2911
+ const opts = {};
2912
+ EDITOR_EVENTS.forEach((event) => {
2913
+ const prefixed = prefixedEventName(event);
2914
+ opts[prefixed] = (props) => {
2915
+ const handlers = this.listeners[event];
2916
+ handlers.forEach((handler) => {
2917
+ handler(props);
2918
+ });
2919
+ };
2920
+ });
2921
+ return opts;
2922
+ }
2923
+ }
2924
+ function resolveRawWysiwygExtension(raw, ctx) {
2925
+ return typeof raw === 'function' ? raw(ctx) : raw;
2926
+ }
2927
+ function resolveRawWysiwygExtensions(raws, ctx) {
2928
+ return raws.map((raw) => resolveRawWysiwygExtension(raw, ctx));
2929
+ }
2877
2930
  function resolveRawWysiwygEditorTool(raw, vui) {
2878
2931
  return typeof raw === 'function' ? raw(vui) : raw;
2879
2932
  }
2880
- function resolveRawWysiwygEditorTools(raws, vui) {
2933
+ function resolveRawWysiwygEditorTools(rawTools, vui) {
2881
2934
  const tools = [];
2882
2935
  const extensions = [];
2883
- raws.forEach((raw) => {
2936
+ rawTools.forEach((raw) => {
2884
2937
  let resolved = resolveRawWysiwygEditorTool(raw, vui);
2885
2938
  if (!Array.isArray(resolved)) {
2886
2939
  resolved = [resolved];
@@ -2911,6 +2964,10 @@ const VWysiwygEditor = vue.defineComponent({
2911
2964
  ...createControlFieldProviderProps(),
2912
2965
  ...createControlProps(),
2913
2966
  ...vueKit.defineSlotsProps(),
2967
+ extensions: {
2968
+ type: Array,
2969
+ default: () => []
2970
+ },
2914
2971
  tools: {
2915
2972
  type: Array,
2916
2973
  default: () => []
@@ -2933,13 +2990,40 @@ const VWysiwygEditor = vue.defineComponent({
2933
2990
  nodeType: VUI_WYSIWYG_EDITOR_SYMBOL,
2934
2991
  validationValue: () => textRef.value
2935
2992
  });
2993
+ const initializeCtx = new WysiwygEditorInitializeContext({
2994
+ onCreate: ({
2995
+ editor
2996
+ }) => {
2997
+ textRef.value = editor.getText();
2998
+ updateEditable();
2999
+ },
3000
+ onFocus: ({
3001
+ event
3002
+ }) => {
3003
+ inputControl.focusHandler(event);
3004
+ },
3005
+ onBlur: ({
3006
+ event
3007
+ }) => {
3008
+ inputControl.blurHandler(event);
3009
+ },
3010
+ onUpdate: ({
3011
+ editor
3012
+ }) => {
3013
+ inputControl.value = editor.getHTML();
3014
+ textRef.value = editor.getText();
3015
+ }
3016
+ });
2936
3017
  const extensions = [StarterKit__default.configure({
2937
3018
  bold: false,
2938
3019
  bulletList: false,
2939
3020
  orderedList: false,
2940
3021
  history: false,
2941
3022
  italic: false
2942
- }), ...wysiwygSettings.value.extensions];
3023
+ }), // Linter.configure({
3024
+ // plugins: [BadWords(['abc', 'evidently']), Punctuation, HeadingLevel],
3025
+ // }),
3026
+ ...resolveRawWysiwygExtensions(props.extensions, initializeCtx), ...wysiwygSettings.value.extensions];
2943
3027
 
2944
3028
  const updateEditable = () => {
2945
3029
  if (!editor.value) return;
@@ -2953,23 +3037,13 @@ const VWysiwygEditor = vue.defineComponent({
2953
3037
  $editor.commands.setContent(modelValue == null ? '' : modelValue, false);
2954
3038
  textRef.value = $editor.getText();
2955
3039
  });
2956
- const editor = vue3.useEditor({
2957
- onCreate: ({
2958
- editor
2959
- }) => {
2960
- textRef.value = editor.getText();
2961
- updateEditable();
2962
- },
2963
- onFocus: ctx => {
2964
- inputControl.focusHandler(ctx.event);
2965
- },
2966
- onBlur: ctx => {
2967
- inputControl.blurHandler(ctx.event);
2968
- },
2969
- onUpdate: ev => {
2970
- inputControl.value = ev.editor.getHTML();
2971
- textRef.value = ev.editor.getText();
2972
- },
3040
+ initializeCtx.on('create', ({
3041
+ editor
3042
+ }) => {
3043
+ textRef.value = editor.getText();
3044
+ updateEditable();
3045
+ });
3046
+ const editor = vue3.useEditor({ ...initializeCtx.editorOptions(),
2973
3047
  autofocus: props.autofocus,
2974
3048
  content: props.modelValue,
2975
3049
  extensions,
@@ -3052,6 +3126,9 @@ const VWysiwygEditor = vue.defineComponent({
3052
3126
  });
3053
3127
  };
3054
3128
 
3129
+ vue.onBeforeUnmount(() => {
3130
+ editor.value && editor.value.destroy();
3131
+ });
3055
3132
  return {
3056
3133
  editor,
3057
3134
  ...inputControl.expose(),
@@ -3087,9 +3164,7 @@ const VWysiwygEditor = vue.defineComponent({
3087
3164
  } = this;
3088
3165
  return vue.createVNode("div", {
3089
3166
  "class": "v-wysiwyg-editor__wrapper"
3090
- }, [!this.isReadonly && this.createTools(), vue.createVNode("div", {
3091
- "class": "v-wysiwyg-editor__body"
3092
- }, [vue.createVNode(VControlField, {
3167
+ }, [!this.isReadonly && this.createTools(), vue.createVNode(VControlField, {
3093
3168
  "class": "v-wysiwyg-editor__input",
3094
3169
  "autoHeight": true,
3095
3170
  "startAdornment": this.startAdornment,
@@ -3099,7 +3174,9 @@ const VWysiwygEditor = vue.defineComponent({
3099
3174
  default: () => {
3100
3175
  let _slot2;
3101
3176
 
3102
- return vue.createVNode(vue.Fragment, null, [vue.createVNode(vue3.EditorContent, {
3177
+ return vue.createVNode("div", {
3178
+ "class": "v-wysiwyg-editor__body"
3179
+ }, [vue.createVNode(vue3.EditorContent, {
3103
3180
  "class": "v-wysiwyg-editor__input__element wysiwyg",
3104
3181
  "editor": editor
3105
3182
  }, null), !this.floatingToolbar && !!editor && !this.isReadonly && vue.createVNode(vue3.BubbleMenu, {
@@ -3109,7 +3186,7 @@ const VWysiwygEditor = vue.defineComponent({
3109
3186
  default: () => [_slot2]
3110
3187
  })]);
3111
3188
  }
3112
- })])]);
3189
+ })]);
3113
3190
  },
3114
3191
  infoAppends: () => {
3115
3192
  const {
@@ -3127,6 +3204,284 @@ function resolveContextableValue(ctx, source) {
3127
3204
  return typeof source === 'function' ? source(ctx) : source;
3128
3205
  }
3129
3206
 
3207
+ const PROBLEM_CLASS_NAME = 'v-linter__problem';
3208
+ const ISSUE_ICON_CLASS_NAME = 'v-linter__issue-icon';
3209
+ const ISSUE_MENU_CLASS_NAME = 'v-linter__issue-menu';
3210
+ function renderIcon(view, issue) {
3211
+ const { level = 'warning' } = issue;
3212
+ const icon = document.createElement('div');
3213
+ icon.className = `${ISSUE_ICON_CLASS_NAME} ${level}-scope`;
3214
+ icon.title = issue.message;
3215
+ icon.issue = issue;
3216
+ return icon;
3217
+ }
3218
+ function renderMenu(view, issue) {
3219
+ const menuWrapper = document.createElement('div');
3220
+ menuWrapper.className = 'v-linter__issue-menu-wrapper';
3221
+ const menu = document.createElement('div');
3222
+ menu.className = `${ISSUE_MENU_CLASS_NAME} elevation-3`;
3223
+ menu.innerText = issue.message;
3224
+ const { fix, fixMessage } = issue;
3225
+ if (fix && fixMessage) {
3226
+ const $fix = document.createElement('button');
3227
+ $fix.type = 'button';
3228
+ $fix.innerHTML = fixMessage;
3229
+ $fix.addEventListener('click', () => {
3230
+ fix(view, issue);
3231
+ });
3232
+ $fix.className = `v-linter__issue-menu__fix`;
3233
+ menu.appendChild($fix);
3234
+ }
3235
+ menu.issue = issue;
3236
+ menuWrapper.appendChild(menu);
3237
+ return menuWrapper;
3238
+ }
3239
+ function getIssueElement(source, className) {
3240
+ if (!source || !(source instanceof HTMLElement)) {
3241
+ return;
3242
+ }
3243
+ if (source.classList.contains(className)) {
3244
+ return source;
3245
+ }
3246
+ const el = source.closest(`.${className}`);
3247
+ if (el) {
3248
+ return el;
3249
+ }
3250
+ }
3251
+ function getIconElement(source) {
3252
+ return getIssueElement(source, ISSUE_ICON_CLASS_NAME);
3253
+ }
3254
+ function getMenuElement(source) {
3255
+ return getIssueElement(source, ISSUE_MENU_CLASS_NAME);
3256
+ }
3257
+ function getProblemElement(source) {
3258
+ return getIssueElement(source, PROBLEM_CLASS_NAME);
3259
+ }
3260
+ function runAllLinterPlugins(doc, plugins) {
3261
+ const decorations = [];
3262
+ const results = plugins
3263
+ .map((RegisteredLinterPlugin) => {
3264
+ return new RegisteredLinterPlugin(doc).scan().getResults();
3265
+ })
3266
+ .flat();
3267
+ results.forEach((issue) => {
3268
+ const { level = 'warning' } = issue;
3269
+ decorations.push(prosemirrorView.Decoration.inline(issue.from, issue.to, {
3270
+ class: `${PROBLEM_CLASS_NAME} ${level}-scope`,
3271
+ 'data-issue': JSON.stringify(issue),
3272
+ title: issue.message,
3273
+ }), prosemirrorView.Decoration.widget(issue.from, (view) => renderIcon(view, issue)), prosemirrorView.Decoration.widget(issue.from, (view) => renderMenu(view, issue)));
3274
+ });
3275
+ return prosemirrorView.DecorationSet.create(doc, decorations);
3276
+ }
3277
+ function updateMenusPosition(editorElement) {
3278
+ const { left, right } = editorElement.getBoundingClientRect();
3279
+ const menus = Array.from(editorElement.querySelectorAll(`.${ISSUE_MENU_CLASS_NAME}`));
3280
+ menus.forEach((menu) => {
3281
+ const { left: menuLeft, right: menuRight } = menu.getBoundingClientRect();
3282
+ const overflow = menuRight - right;
3283
+ let offset = 0;
3284
+ if (overflow > 0) {
3285
+ offset = -overflow;
3286
+ if (menuLeft + offset < left) {
3287
+ offset = 0;
3288
+ }
3289
+ }
3290
+ menu.style.transform = `translateX(${offset}px)`;
3291
+ });
3292
+ }
3293
+ const debouncedUpdateMenusPosition = helpers.debounce(updateMenusPosition, 250);
3294
+ const Linter = core.Extension.create({
3295
+ name: 'linter',
3296
+ addOptions() {
3297
+ return {
3298
+ plugins: [],
3299
+ };
3300
+ },
3301
+ onCreate() {
3302
+ const { dom } = this.editor.view;
3303
+ debouncedUpdateMenusPosition(dom);
3304
+ },
3305
+ onUpdate() {
3306
+ const { dom } = this.editor.view;
3307
+ debouncedUpdateMenusPosition(dom);
3308
+ },
3309
+ addProseMirrorPlugins() {
3310
+ const { plugins } = this.options;
3311
+ return [
3312
+ new prosemirrorState.Plugin({
3313
+ key: new prosemirrorState.PluginKey('linter'),
3314
+ state: {
3315
+ init(_, { doc }) {
3316
+ return runAllLinterPlugins(doc, plugins);
3317
+ },
3318
+ apply(transaction, oldState) {
3319
+ return transaction.docChanged
3320
+ ? runAllLinterPlugins(transaction.doc, plugins)
3321
+ : oldState;
3322
+ },
3323
+ },
3324
+ props: {
3325
+ decorations(state) {
3326
+ return this.getState(state);
3327
+ },
3328
+ handleClick(view, _, event) {
3329
+ const activeMenus = Array.from(view.dom.querySelectorAll(`.${ISSUE_MENU_CLASS_NAME}--active`));
3330
+ activeMenus.forEach((menu) => menu.classList.remove(`${ISSUE_MENU_CLASS_NAME}--active`));
3331
+ const problem = getProblemElement(event.target);
3332
+ const issueString = problem && problem.dataset['issue'];
3333
+ const issue = issueString && JSON.parse(issueString);
3334
+ if (issue) {
3335
+ const menuWrapper = problem.previousElementSibling;
3336
+ const menu = menuWrapper &&
3337
+ menuWrapper.querySelector(`.${ISSUE_MENU_CLASS_NAME}`);
3338
+ menu && menu.classList.add(`${ISSUE_MENU_CLASS_NAME}--active`);
3339
+ const { from, to } = issue;
3340
+ view.dispatch(view.state.tr
3341
+ .setSelection(prosemirrorState.TextSelection.create(view.state.doc, from, to))
3342
+ .scrollIntoView());
3343
+ return true;
3344
+ }
3345
+ const menu = getMenuElement(event.target);
3346
+ if (menu) {
3347
+ menu.classList.add(`${ISSUE_MENU_CLASS_NAME}--active`);
3348
+ }
3349
+ const target = getIconElement(event.target);
3350
+ if (target && target.issue) {
3351
+ const menuWrapper = target.nextElementSibling;
3352
+ const menu = menuWrapper &&
3353
+ menuWrapper.querySelector(`.${ISSUE_MENU_CLASS_NAME}`);
3354
+ menu && menu.classList.add(`${ISSUE_MENU_CLASS_NAME}--active`);
3355
+ const { from, to } = target.issue;
3356
+ view.dispatch(view.state.tr
3357
+ .setSelection(prosemirrorState.TextSelection.create(view.state.doc, from, to))
3358
+ .scrollIntoView());
3359
+ return true;
3360
+ }
3361
+ return false;
3362
+ },
3363
+ handleDoubleClick(view, _, event) {
3364
+ const target = getIconElement(event.target);
3365
+ if (target && target.issue) {
3366
+ const prob = target.issue;
3367
+ if (prob.fix) {
3368
+ prob.fix(view, prob);
3369
+ view.focus();
3370
+ return true;
3371
+ }
3372
+ }
3373
+ return false;
3374
+ },
3375
+ },
3376
+ }),
3377
+ ];
3378
+ },
3379
+ });
3380
+
3381
+ class LinterPlugin {
3382
+ doc;
3383
+ results = [];
3384
+ constructor(doc) {
3385
+ this.doc = doc;
3386
+ }
3387
+ record(result) {
3388
+ this.results.push({
3389
+ level: 'error',
3390
+ ...result,
3391
+ });
3392
+ }
3393
+ scan() {
3394
+ return this;
3395
+ }
3396
+ getResults() {
3397
+ return this.results;
3398
+ }
3399
+ }
3400
+
3401
+ function BadWords(words) {
3402
+ const regex = new RegExp(`\\b(${words.join('|')})\\b`);
3403
+ return class BadWords extends LinterPlugin {
3404
+ scan() {
3405
+ this.doc.descendants((node, position) => {
3406
+ if (!node.isText) {
3407
+ return;
3408
+ }
3409
+ const matches = regex.exec(node.text);
3410
+ if (matches) {
3411
+ const fixValue = matches[0] + '!!!!!';
3412
+ this.record({
3413
+ level: 'warning',
3414
+ message: `Try not to say '${matches[0]}'`,
3415
+ from: position + matches.index,
3416
+ to: position + matches.index + matches[0].length,
3417
+ fix: () => {
3418
+ console.log('hoge');
3419
+ },
3420
+ fixMessage: `「${fixValue}」に修正する。`,
3421
+ });
3422
+ }
3423
+ });
3424
+ return this;
3425
+ }
3426
+ };
3427
+ }
3428
+
3429
+ class HeadingLevel extends LinterPlugin {
3430
+ fixHeader(level) {
3431
+ return function ({ state, dispatch }, issue) {
3432
+ dispatch(state.tr.setNodeMarkup(issue.from - 1, undefined, { level }));
3433
+ };
3434
+ }
3435
+ scan() {
3436
+ let lastHeadLevel = null;
3437
+ this.doc.descendants((node, position) => {
3438
+ if (node.type.name === 'heading') {
3439
+ // Check whether heading levels fit under the current level
3440
+ const { level } = node.attrs;
3441
+ if (lastHeadLevel != null && level > lastHeadLevel + 1) {
3442
+ this.record({
3443
+ message: `Heading too small (${level} under ${lastHeadLevel})`,
3444
+ from: position + 1,
3445
+ to: position + 1 + node.content.size,
3446
+ fix: this.fixHeader(lastHeadLevel + 1),
3447
+ });
3448
+ }
3449
+ lastHeadLevel = level;
3450
+ }
3451
+ });
3452
+ return this;
3453
+ }
3454
+ }
3455
+
3456
+ class Punctuation extends LinterPlugin {
3457
+ regex = / ([,.!?:]) ?/g;
3458
+ fix(replacement) {
3459
+ return function ({ state, dispatch }, issue) {
3460
+ dispatch(state.tr.replaceWith(issue.from, issue.to, state.schema.text(replacement)));
3461
+ };
3462
+ }
3463
+ scan() {
3464
+ this.doc.descendants((node, position) => {
3465
+ if (!node.isText) {
3466
+ return;
3467
+ }
3468
+ if (!node.text) {
3469
+ return;
3470
+ }
3471
+ const matches = this.regex.exec(node.text);
3472
+ if (matches) {
3473
+ this.record({
3474
+ message: 'Suspicious spacing around punctuation',
3475
+ from: position + matches.index,
3476
+ to: position + matches.index + matches[0].length,
3477
+ fix: this.fix(`${matches[1]} `),
3478
+ });
3479
+ }
3480
+ });
3481
+ return this;
3482
+ }
3483
+ }
3484
+
3130
3485
  const WysiwygBulletListTool = (vui, options) => {
3131
3486
  const tool = {
3132
3487
  key: 'bulletList',
@@ -5052,11 +5407,16 @@ exports.VSnackbar = vueKit.VSnackbar;
5052
5407
  exports.VTooltip = vueKit.VTooltip;
5053
5408
  exports.ICON_NAMES = iconFont.ICON_NAMES;
5054
5409
  exports.AVATAR_SIZES = AVATAR_SIZES;
5410
+ exports.BadWords = BadWords;
5055
5411
  exports.CHIP_SIZES = CHIP_SIZES;
5056
5412
  exports.CONTROL_FIELD_VARIANTS = CONTROL_FIELD_VARIANTS;
5057
5413
  exports.CONTROL_LOADING_SPINNER_SIZES = CONTROL_LOADING_SPINNER_SIZES;
5058
5414
  exports.CONTROL_SIZES = CONTROL_SIZES;
5415
+ exports.HeadingLevel = HeadingLevel;
5416
+ exports.Linter = Linter;
5417
+ exports.LinterPlugin = LinterPlugin;
5059
5418
  exports.PAGINATION_ALIGNS = PAGINATION_ALIGNS;
5419
+ exports.Punctuation = Punctuation;
5060
5420
  exports.VApp = VApp;
5061
5421
  exports.VAvatar = VAvatar;
5062
5422
  exports.VBreadcrumbs = VBreadcrumbs;
@@ -5119,6 +5479,7 @@ exports.VuiInjectionKey = VuiInjectionKey;
5119
5479
  exports.VuiPlugin = VuiPlugin;
5120
5480
  exports.VuiService = VuiService;
5121
5481
  exports.WysiwygBulletListTool = WysiwygBulletListTool;
5482
+ exports.WysiwygEditorInitializeContext = WysiwygEditorInitializeContext;
5122
5483
  exports.WysiwygFormatBoldTool = WysiwygFormatBoldTool;
5123
5484
  exports.WysiwygFormatItalicTool = WysiwygFormatItalicTool;
5124
5485
  exports.WysiwygFormatUnderlineTool = WysiwygFormatUnderlineTool;
@@ -5153,6 +5514,7 @@ exports.renderNavigationItemInput = renderNavigationItemInput;
5153
5514
  exports.resolveNavigationItemInput = resolveNavigationItemInput;
5154
5515
  exports.resolveRawIconProp = resolveRawIconProp;
5155
5516
  exports.resolveRawWysiwygEditorTools = resolveRawWysiwygEditorTools;
5517
+ exports.resolveRawWysiwygExtensions = resolveRawWysiwygExtensions;
5156
5518
  exports.splitAttrs = splitAttrs;
5157
5519
  exports.useControl = useControl;
5158
5520
  exports.useControlField = useControlField;