Start work on inline tools
This commit is contained in:
parent
e3ddcefb30
commit
0fcb0d03a0
|
@ -21,3 +21,7 @@ export default {
|
||||||
view: defineAsyncComponent(() => import('./view')),
|
view: defineAsyncComponent(() => import('./view')),
|
||||||
} as IBlockDefinition<any>;
|
} as IBlockDefinition<any>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Go by example
|
||||||
|
|
||||||
|
As Schlechtenburg is still in active development, it's good to check out the official blocks to see what they look like.
|
||||||
|
|
|
@ -164,6 +164,7 @@ export const SbBlock = defineComponent({
|
||||||
eventRemoveSelf={props.eventRemoveSelf}
|
eventRemoveSelf={props.eventRemoveSelf}
|
||||||
eventActivatePrevious={props.eventActivatePrevious}
|
eventActivatePrevious={props.eventActivatePrevious}
|
||||||
eventActivateNext={props.eventActivateNext}
|
eventActivateNext={props.eventActivateNext}
|
||||||
|
data-sb-block--content
|
||||||
|
|
||||||
{...{
|
{...{
|
||||||
onClick: ($event: MouseEvent) => {
|
onClick: ($event: MouseEvent) => {
|
||||||
|
|
76
packages/core/lib/components/Contenteditable.tsx
Normal file
76
packages/core/lib/components/Contenteditable.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
h,
|
||||||
|
defineComponent,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
PropType,
|
||||||
|
watchEffect,
|
||||||
|
} from 'vue';
|
||||||
|
import { useInline } from '../inline';
|
||||||
|
|
||||||
|
export const SbContenteditable = defineComponent({
|
||||||
|
name: 'sb-contenteditable',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
inputRef: {
|
||||||
|
type: (null as unknown) as PropType<Ref<HTMLElement|null>>,
|
||||||
|
default: ref(null),
|
||||||
|
},
|
||||||
|
tag: { type: String, default: 'div' },
|
||||||
|
|
||||||
|
value: { type: String, default: '' },
|
||||||
|
|
||||||
|
onValueChange: {
|
||||||
|
type: (null as unknown) as PropType<(value: string) => void>,
|
||||||
|
default: (_:string) => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const {
|
||||||
|
registerClient,
|
||||||
|
unregisterClient,
|
||||||
|
} = useInline();
|
||||||
|
const onKeyup = (event: KeyboardEvent) => {
|
||||||
|
if (event.code !== 'Backspace' && event.code !== 'Delete') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.inputRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textContent = props.inputRef.value.textContent;
|
||||||
|
if (textContent === '') {
|
||||||
|
props.inputRef.value.innerHTML = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.inputRef.value?.focus();
|
||||||
|
registerClient(props.inputRef.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.inputRef.value && props.inputRef.value.innerHTML != props.value) {
|
||||||
|
props.inputRef.value.innerHTML = props.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unregisterClient(props.inputRef.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => h(props.tag, {
|
||||||
|
class: 'sb-contenteditable',
|
||||||
|
contenteditable: 'true',
|
||||||
|
ref: props.inputRef,
|
||||||
|
onKeyup,
|
||||||
|
onInput: () => {
|
||||||
|
props.onValueChange(props.inputRef.value?.innerHTML || '');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
provide,
|
provide,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
|
shallowRef,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
computed,
|
computed,
|
||||||
|
@ -15,6 +16,8 @@ import {
|
||||||
IBlockLibrary,
|
IBlockLibrary,
|
||||||
ITreeNode,
|
ITreeNode,
|
||||||
OnUpdateBlockCb,
|
OnUpdateBlockCb,
|
||||||
|
IInlineToolDefinition,
|
||||||
|
IInlineToolLibrary,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { model } from '../block-helpers';
|
import { model } from '../block-helpers';
|
||||||
import { SymMode, SbMode } from '../mode';
|
import { SymMode, SbMode } from '../mode';
|
||||||
|
@ -26,12 +29,19 @@ import {
|
||||||
} from '../use-block-tree';
|
} from '../use-block-tree';
|
||||||
import { SymEditorDimensions, useResizeObserver } from '../use-resize-observer';
|
import { SymEditorDimensions, useResizeObserver } from '../use-resize-observer';
|
||||||
import { SymActiveBlock } from '../use-activation';
|
import { SymActiveBlock } from '../use-activation';
|
||||||
|
import { SymRootElement } from '../use-root-element';
|
||||||
|
import {
|
||||||
|
SbInlineToolbar,
|
||||||
|
SymInlineToolbarClients,
|
||||||
|
SymInlineToolLibrary,
|
||||||
|
} from '../inline';
|
||||||
|
|
||||||
import { SbMainMenu } from './MainMenu';
|
import { SbMainMenu } from './MainMenu';
|
||||||
import { SbBlock } from './Block';
|
import { SbBlock } from './Block';
|
||||||
|
|
||||||
export interface ISbMainProps {
|
export interface ISbMainProps {
|
||||||
availableBlocks: IBlockDefinition<any>[];
|
availableBlocks: IBlockDefinition<any>[];
|
||||||
|
availableInlineTools: IInlineToolDefinition[];
|
||||||
block: IBlockData<any>;
|
block: IBlockData<any>;
|
||||||
eventUpdate: OnUpdateBlockCb;
|
eventUpdate: OnUpdateBlockCb;
|
||||||
mode: SbMode;
|
mode: SbMode;
|
||||||
|
@ -49,6 +59,10 @@ export const SbMain = defineComponent({
|
||||||
type: Array as PropType<IBlockDefinition<any>[]>,
|
type: Array as PropType<IBlockDefinition<any>[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
availableInlineTools: {
|
||||||
|
type: Array as PropType<IInlineToolDefinition[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
block: {
|
block: {
|
||||||
type: Object as PropType<IBlockData<any>>,
|
type: Object as PropType<IBlockData<any>>,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -72,6 +86,10 @@ export const SbMain = defineComponent({
|
||||||
setup(props: ISbMainProps) {
|
setup(props: ISbMainProps) {
|
||||||
const el: Ref<null|HTMLElement> = ref(null);
|
const el: Ref<null|HTMLElement> = ref(null);
|
||||||
useResizeObserver(el, SymEditorDimensions);
|
useResizeObserver(el, SymEditorDimensions);
|
||||||
|
provide(SymRootElement, el);
|
||||||
|
|
||||||
|
const inlineClients = shallowRef([]);
|
||||||
|
provide(SymInlineToolbarClients, inlineClients);
|
||||||
|
|
||||||
const mode = ref(props.mode);
|
const mode = ref(props.mode);
|
||||||
provide(SymMode, mode);
|
provide(SymMode, mode);
|
||||||
|
@ -99,9 +117,16 @@ export const SbMain = defineComponent({
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
provide(SymBlockLibrary, blockLibrary);
|
provide(SymBlockLibrary, blockLibrary);
|
||||||
|
|
||||||
|
const inlineToolLibrary: IInlineToolLibrary = shallowReactive({
|
||||||
|
...props.availableInlineTools.reduce(
|
||||||
|
(tools: IInlineToolLibrary, tool: IInlineToolDefinition) => ({ ...tools, [tool.name]: tool }),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
provide(SymInlineToolLibrary, inlineToolLibrary);
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div
|
<div
|
||||||
class={classes.value}
|
class={classes.value}
|
||||||
|
@ -123,6 +148,7 @@ export const SbMain = defineComponent({
|
||||||
block={props.block}
|
block={props.block}
|
||||||
eventUpdate={props.eventUpdate}
|
eventUpdate={props.eventUpdate}
|
||||||
/>
|
/>
|
||||||
|
<SbInlineToolbar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export * from './mode';
|
export * from './mode';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
|
export * from './inline';
|
||||||
|
|
||||||
export * from './block-helpers';
|
export * from './block-helpers';
|
||||||
|
|
||||||
export * from './use-activation';
|
export * from './use-activation';
|
||||||
|
@ -8,10 +10,14 @@ export * from './use-dynamic-blocks';
|
||||||
export * from './use-resize-observer';
|
export * from './use-resize-observer';
|
||||||
|
|
||||||
export * from './components/Main';
|
export * from './components/Main';
|
||||||
|
|
||||||
export * from './components/Block';
|
export * from './components/Block';
|
||||||
export * from './components/BlockPicker';
|
export * from './components/BlockPicker';
|
||||||
export * from './components/BlockOrdering';
|
export * from './components/BlockOrdering';
|
||||||
export * from './components/BlockPlaceholder';
|
export * from './components/BlockPlaceholder';
|
||||||
|
|
||||||
|
export * from './components/Contenteditable';
|
||||||
|
|
||||||
export * from './components/Toolbar';
|
export * from './components/Toolbar';
|
||||||
export * from './components/Button';
|
export * from './components/Button';
|
||||||
export * from './components/Select';
|
export * from './components/Select';
|
||||||
|
|
12
packages/core/lib/inline/InlineToolbar.scss
Normal file
12
packages/core/lib/inline/InlineToolbar.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.sb-inline-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--bg);
|
||||||
|
height: 2rem;
|
||||||
|
min-width: 2rem;
|
||||||
|
display: flex;
|
||||||
|
z-index: var(--z-toolbar);
|
||||||
|
|
||||||
|
&_hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
115
packages/core/lib/inline/InlineToolbar.tsx
Normal file
115
packages/core/lib/inline/InlineToolbar.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { useRootElement } from '../use-root-element';
|
||||||
|
import { useInline } from './use-inline';
|
||||||
|
import {
|
||||||
|
getSelection,
|
||||||
|
getAnchorElementForSelection,
|
||||||
|
getRangeFromSelection,
|
||||||
|
getRect,
|
||||||
|
} from './selection';
|
||||||
|
|
||||||
|
import './InlineToolbar.scss';
|
||||||
|
|
||||||
|
export const SbInlineToolbar = defineComponent({
|
||||||
|
name: 'sb-inlinetoolbar',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const { rootElement } = useRootElement();
|
||||||
|
const {
|
||||||
|
clients,
|
||||||
|
getAllTools,
|
||||||
|
} = useInline();
|
||||||
|
|
||||||
|
const allTools = computed(() => getAllTools());
|
||||||
|
|
||||||
|
const selectionRect: Ref<DOMRect|null>= ref(null);
|
||||||
|
const updateSelectionRect = () => {
|
||||||
|
const selection = getSelection();
|
||||||
|
if (!selection) {
|
||||||
|
console.warn('Could not get selection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectionRect.value = getRect(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showing: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
showing.value = true;
|
||||||
|
updateSelectionRect();
|
||||||
|
};
|
||||||
|
const hide = () => { showing.value = false; };
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
const rootRect = rootElement.value?.getBoundingClientRect();
|
||||||
|
const x = (selectionRect.value?.x || 0)
|
||||||
|
- (rootRect?.left || 0);
|
||||||
|
const y = (selectionRect.value?.y || 0)
|
||||||
|
+ (selectionRect.value?.height || 0)
|
||||||
|
- (rootRect?.top || 0);
|
||||||
|
return {
|
||||||
|
left: Math.floor(x) + 'px',
|
||||||
|
top: Math.floor(y) + 'px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'sb-inline-toolbar': true,
|
||||||
|
'sb-inline-toolbar_hidden': !showing.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSelectionChanged = debounce(() => {
|
||||||
|
const selection = getSelection();
|
||||||
|
if (!selection) {
|
||||||
|
console.warn('Could not get selection');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = getRangeFromSelection(selection);
|
||||||
|
// If we're not selecting anything, bail
|
||||||
|
if (!range || range.endOffset === range.startOffset) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedElement = getAnchorElementForSelection(selection);
|
||||||
|
if (!focusedElement) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new selection is not in registered clients, close it
|
||||||
|
if (!clients.value.find(client => client === focusedElement)) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
show();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
rootElement.value?.addEventListener('click', close);
|
||||||
|
document.addEventListener('selectionchange', onSelectionChanged, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
rootElement.value?.removeEventListener('click', close);
|
||||||
|
document.removeEventListener('selectionchange', onSelectionChanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => allTools.value.length
|
||||||
|
? <div
|
||||||
|
style={style.value}
|
||||||
|
class={classes.value}
|
||||||
|
>{allTools.value.map(tool => tool.ui)}</div>
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
});
|
37
packages/core/lib/inline/dom.ts
Normal file
37
packages/core/lib/inline/dom.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright notice:
|
||||||
|
*
|
||||||
|
* Large parts of this file are heavily inspired if not downright copied from editor.js,
|
||||||
|
* copyright MIT.
|
||||||
|
* https://editorjs.io/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if object is DOM node
|
||||||
|
*
|
||||||
|
* @param {*} node - object to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isElement = (node: any): node is Element => {
|
||||||
|
if (node instanceof Number) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if object is DocumentFragment node
|
||||||
|
*
|
||||||
|
* @param {object} node - object to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isFragment = (node: any): node is DocumentFragment => {
|
||||||
|
if (node instanceof Number) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||||
|
}
|
4
packages/core/lib/inline/index.ts
Normal file
4
packages/core/lib/inline/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './InlineToolbar';
|
||||||
|
export * from './use-inline';
|
||||||
|
export * from './selection';
|
||||||
|
export * from './dom';
|
93
packages/core/lib/inline/selection.ts
Normal file
93
packages/core/lib/inline/selection.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright notice:
|
||||||
|
*
|
||||||
|
* Large parts of this file are heavily inspired if not downright copied from editor.js,
|
||||||
|
* copyright MIT.
|
||||||
|
* https://editorjs.io/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isElement } from './dom';
|
||||||
|
|
||||||
|
export const getSelection = globalThis.getSelection ? globalThis.getSelection : () => null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns range from passed Selection object
|
||||||
|
*
|
||||||
|
* @param selection - Selection object to get Range from
|
||||||
|
*/
|
||||||
|
export const getRangeFromSelection = (selection: Selection): Range|null => {
|
||||||
|
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns selected anchor element
|
||||||
|
*
|
||||||
|
* @returns {Element|null}
|
||||||
|
*/
|
||||||
|
export const getAnchorElementForSelection = (selection: Selection): Element | null => {
|
||||||
|
const anchorNode = selection.anchorNode;
|
||||||
|
|
||||||
|
if (!anchorNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isElement(anchorNode)) {
|
||||||
|
return anchorNode.parentElement;
|
||||||
|
} else {
|
||||||
|
return anchorNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates position and size of selected text
|
||||||
|
*
|
||||||
|
* @returns {DOMRect}
|
||||||
|
*/
|
||||||
|
export const getRect = (selection: Selection): DOMRect => {
|
||||||
|
const defaultRect = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
if (selection.rangeCount === null || isNaN(selection.rangeCount)) {
|
||||||
|
console.warn('Method SelectionUtils.rangeCount is not supported');
|
||||||
|
return defaultRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.rangeCount === 0) {
|
||||||
|
return defaultRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0).cloneRange() as Range;
|
||||||
|
|
||||||
|
let rect = { ...defaultRect };
|
||||||
|
|
||||||
|
if (range.getBoundingClientRect) {
|
||||||
|
rect = range.getBoundingClientRect() as DOMRect;
|
||||||
|
}
|
||||||
|
// Fall back to inserting a temporary element
|
||||||
|
if (rect.x === 0 && rect.y === 0) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
|
||||||
|
if (span.getBoundingClientRect) {
|
||||||
|
// Ensure span has dimensions and position by
|
||||||
|
// adding a zero-width space character
|
||||||
|
span.appendChild(document.createTextNode('\u200b'));
|
||||||
|
range.insertNode(span);
|
||||||
|
rect = span.getBoundingClientRect() as DOMRect;
|
||||||
|
|
||||||
|
const spanParent = span.parentNode;
|
||||||
|
if (spanParent) {
|
||||||
|
spanParent.removeChild(span);
|
||||||
|
|
||||||
|
// Glue any broken text nodes back together
|
||||||
|
spanParent.normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
};
|
49
packages/core/lib/inline/use-inline.ts
Normal file
49
packages/core/lib/inline/use-inline.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
inject,
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
import { IInlineToolLibrary } from '../types';
|
||||||
|
|
||||||
|
export const SymInlineToolbarClients = Symbol('Schlechtenburg inline toolbar client elements');
|
||||||
|
export const SymInlineToolLibrary = Symbol('Schlechtenburg inline tool library');
|
||||||
|
|
||||||
|
export const useInline = () => {
|
||||||
|
const clients: Ref<HTMLElement[]> = inject(SymInlineToolbarClients, ref([]));
|
||||||
|
const tools: IInlineToolLibrary = inject(SymInlineToolLibrary, reactive({}));
|
||||||
|
const getTool = (name: string) => tools[name];
|
||||||
|
const getAllTools = () => Object.keys(tools).map(name => tools[name]);
|
||||||
|
|
||||||
|
const setClients = (newClients: HTMLElement[]) => {
|
||||||
|
clients.value = newClients;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientIsRegistered = (el: HTMLElement) => !!clients.value.find(cl => cl === el);
|
||||||
|
|
||||||
|
const registerClient = (el: HTMLElement) => {
|
||||||
|
if (clientIsRegistered(el)) {
|
||||||
|
console.warn('Not reregistering toolbar client that is already registered:', el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setClients([
|
||||||
|
...clients.value,
|
||||||
|
el,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregisterClient = (el: HTMLElement) => {
|
||||||
|
setClients(clients.value.filter(cl => cl !== el));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tools,
|
||||||
|
getTool,
|
||||||
|
getAllTools,
|
||||||
|
|
||||||
|
clients,
|
||||||
|
registerClient,
|
||||||
|
unregisterClient,
|
||||||
|
};
|
||||||
|
};
|
|
@ -170,3 +170,22 @@ export interface IBlockDefinition<T> {
|
||||||
export interface IBlockLibrary {
|
export interface IBlockLibrary {
|
||||||
[name: string]: IBlockDefinition<any>;
|
[name: string]: IBlockDefinition<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any Block that you create
|
||||||
|
*/
|
||||||
|
export interface IInlineToolDefinition {
|
||||||
|
name: string;
|
||||||
|
ui: Component<any>;
|
||||||
|
surround: (range: Range) => void;
|
||||||
|
checkState: (selection: Selection) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schlechtenburg maintains a library of inline tools that are available
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface IInlineToolLibrary {
|
||||||
|
[name: string]: IInlineToolDefinition;
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
export const SymActiveBlock = Symbol('Schlechtenburg active block');
|
export const SymActiveBlock = Symbol('Schlechtenburg active block');
|
||||||
export function useActivation(currentBlockId: string|null = null) {
|
export const useActivation = (currentBlockId: string|null = null) => {
|
||||||
const activeBlockId: Ref<string|null> = inject(SymActiveBlock, ref(null));
|
const activeBlockId: Ref<string|null> = inject(SymActiveBlock, ref(null));
|
||||||
|
|
||||||
const isActive = computed(() => activeBlockId.value === currentBlockId);
|
const isActive = computed(() => activeBlockId.value === currentBlockId);
|
||||||
|
@ -46,4 +46,4 @@ export function useActivation(currentBlockId: string|null = null) {
|
||||||
deactivate,
|
deactivate,
|
||||||
requestActivation,
|
requestActivation,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -10,14 +10,12 @@ import {
|
||||||
ITreeNode,
|
ITreeNode,
|
||||||
IBlockData,
|
IBlockData,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { useDynamicBlocks } from './use-dynamic-blocks';
|
|
||||||
import { SbMode } from './mode';
|
|
||||||
|
|
||||||
export const SymBlockTree= Symbol('Schlechtenburg block tree');
|
export const SymBlockTree= Symbol('Schlechtenburg block tree');
|
||||||
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
|
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
|
||||||
export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregister');
|
export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregister');
|
||||||
|
|
||||||
export function useBlockTree() {
|
export const useBlockTree = () => {
|
||||||
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
|
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
|
||||||
const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
|
const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
|
||||||
const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
|
const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
|
||||||
|
@ -52,8 +50,6 @@ export function useBlockTree() {
|
||||||
self.children = self.children.filter((child: ITreeNode) => child.id !== id);
|
self.children = self.children.filter((child: ITreeNode) => child.id !== id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mode } = useDynamicBlocks();
|
|
||||||
|
|
||||||
const register = (block: IBlockData<any>, index: number = 0) => {
|
const register = (block: IBlockData<any>, index: number = 0) => {
|
||||||
if (!block.id) {
|
if (!block.id) {
|
||||||
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
|
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
|
||||||
|
@ -77,4 +73,4 @@ export function useBlockTree() {
|
||||||
blockTree,
|
blockTree,
|
||||||
register,
|
register,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface BlockRect {
|
||||||
|
|
||||||
export const SymBlockDimensions = Symbol('Schlechtenburg block dimensions');
|
export const SymBlockDimensions = Symbol('Schlechtenburg block dimensions');
|
||||||
export const SymEditorDimensions = Symbol('Schlechtenburg editor dimensions');
|
export const SymEditorDimensions = Symbol('Schlechtenburg editor dimensions');
|
||||||
export function useResizeObserver(el: Ref<null|HTMLElement>, symbol: symbol) {
|
export const useResizeObserver = (el: Ref<null|HTMLElement>, symbol: symbol) => {
|
||||||
const dimensions: Ref<null|BlockRect> = ref(null);
|
const dimensions: Ref<null|BlockRect> = ref(null);
|
||||||
provide(symbol, dimensions);
|
provide(symbol, dimensions);
|
||||||
const triggerSizeCalculation = () => {
|
const triggerSizeCalculation = () => {
|
||||||
|
@ -47,7 +47,7 @@ export function useResizeObserver(el: Ref<null|HTMLElement>, symbol: symbol) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return { triggerSizeCalculation, dimensions };
|
return { triggerSizeCalculation, dimensions };
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useBlockSizing() {
|
export function useBlockSizing() {
|
||||||
const editorDimensions: Ref<BlockRect|null> = inject(SymEditorDimensions, ref(null));
|
const editorDimensions: Ref<BlockRect|null> = inject(SymEditorDimensions, ref(null));
|
||||||
|
|
11
packages/core/lib/use-root-element.ts
Normal file
11
packages/core/lib/use-root-element.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {
|
||||||
|
Ref,
|
||||||
|
ref,
|
||||||
|
inject,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
export const SymRootElement = Symbol('Schlechtenburg root element');
|
||||||
|
export const useRootElement = () => {
|
||||||
|
const rootElement: Ref<HTMLElement|null> = inject(SymRootElement, ref(null));
|
||||||
|
return { rootElement };
|
||||||
|
};
|
|
@ -43,7 +43,7 @@ body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--admin-nav {
|
&--edit-nav {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -62,7 +62,7 @@ body {
|
||||||
@media screen and (min-width: 1000px) {
|
@media screen and (min-width: 1000px) {
|
||||||
--ex-nav-width: var(--ex-nav-expanded-width);
|
--ex-nav-width: var(--ex-nav-expanded-width);
|
||||||
|
|
||||||
&--admin-nav {
|
&--edit-nav {
|
||||||
position: unset;
|
position: unset;
|
||||||
width: unset;
|
width: unset;
|
||||||
flex-basis: var(--ex-nav-width);
|
flex-basis: var(--ex-nav-width);
|
||||||
|
|
|
@ -3,14 +3,14 @@ import { NuxtPage } from '#components';
|
||||||
|
|
||||||
import './app.scss';
|
import './app.scss';
|
||||||
|
|
||||||
const AdminNav = defineAsyncComponent(() => import('~~/components/admin/Nav'));
|
const AdminNav = defineAsyncComponent(() => import('~~/components/_/Nav'));
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { me } = useMe();
|
const { me } = useMe();
|
||||||
return () => (
|
return () => (
|
||||||
<div class="ex-app">
|
<div class="ex-app">
|
||||||
{me.value ? <AdminNav class="ex-app--admin-nav" /> : null}
|
{me.value ? <AdminNav class="ex-app--edit-nav" /> : null}
|
||||||
<NuxtPage class="ex-app--page" />
|
<NuxtPage class="ex-app--page" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
.ex-admin-nav {
|
.ex-edit-nav {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: var(--ex-nav-width);
|
width: var(--ex-nav-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
transition: width 0.2s ease;
|
|
||||||
|
.icon {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&_expanded {
|
&_expanded {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -54,24 +58,17 @@
|
||||||
width: calc(100% - 60px);
|
width: calc(100% - 60px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-left: 16px;
|
|
||||||
transition: opacity 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: calc((var(--ex-nav-width) / 2) - 8px);
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_expanded &--menu-item {
|
&_expanded &--menu-item {
|
||||||
&-title {
|
&-action {
|
||||||
//width: calc(100% - (32px + 24px));
|
justify-content: flex-start;
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-title {
|
||||||
|
opacity: 1;
|
||||||
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
67
packages/example-site/components/_/Nav.tsx
Normal file
67
packages/example-site/components/_/Nav.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { NuxtLink } from '#components';
|
||||||
|
|
||||||
|
import './Nav.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { setMe } = useMe();
|
||||||
|
|
||||||
|
const expanded = useState(() => false);
|
||||||
|
const toggle = () => {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'ex-edit-nav': true,
|
||||||
|
'ex-edit-nav_expanded': expanded.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setMe(null);
|
||||||
|
useGqlToken({
|
||||||
|
token: null,
|
||||||
|
config: { type: 'Bearer' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<nav class={classes.value}>
|
||||||
|
<button
|
||||||
|
class="ex-edit-nav--toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggle()}
|
||||||
|
aria-label="Toggle"
|
||||||
|
>
|
||||||
|
{expanded.value
|
||||||
|
? <Icon name="icon-park-solid:expand-right" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
: <Icon name="icon-park-solid:expand-left" class="ex-edit-nav--menu-item-icon" />}
|
||||||
|
</button>
|
||||||
|
<ul class="ex-edit-nav--menu">
|
||||||
|
<li class="ex-edit-nav--menu-item">
|
||||||
|
<NuxtLink
|
||||||
|
class="ex-edit-nav--menu-item-action"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:web" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
<span class="ex-edit-nav--menu-item-title">Website</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="ex-edit-nav--menu-spacer"></li>
|
||||||
|
|
||||||
|
<li class="ex-edit-nav--menu-item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ex-edit-nav--menu-item-action"
|
||||||
|
onClick={() => logout()}
|
||||||
|
>
|
||||||
|
<Icon name="mdi:logout" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
<span class="ex-edit-nav--menu-item-title">Logout</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,73 +0,0 @@
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { NuxtLink } from '#components';
|
|
||||||
|
|
||||||
import './Nav.scss';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
setup() {
|
|
||||||
const { setMe } = useMe();
|
|
||||||
|
|
||||||
const expanded = useState(() => false);
|
|
||||||
const toggle = () => {
|
|
||||||
expanded.value = !expanded.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = computed(() => ({
|
|
||||||
'ex-admin-nav': true,
|
|
||||||
'ex-admin-nav_expanded': expanded.value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
setMe(null);
|
|
||||||
useGqlToken({
|
|
||||||
token: null,
|
|
||||||
config: { type: 'Bearer' },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => (
|
|
||||||
<nav class={classes.value}>
|
|
||||||
<button
|
|
||||||
class="ex-admin-nav--toggle"
|
|
||||||
type="button"
|
|
||||||
onClick={() => toggle()}
|
|
||||||
aria-label="Toggle"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
icon={`fa-solid fa-arrow-${expanded.value ? 'left' : 'right'}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<ul class="ex-admin-nav--menu">
|
|
||||||
<li class="ex-admin-nav--menu-item">
|
|
||||||
<NuxtLink
|
|
||||||
class="ex-admin-nav--menu-item-action"
|
|
||||||
to="/"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="ex-admin-nav--menu-item-icon"
|
|
||||||
icon="fa-solid fa-home"
|
|
||||||
/>
|
|
||||||
<span class="ex-admin-nav--menu-item-title">Website</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="ex-admin-nav--menu-spacer"></li>
|
|
||||||
|
|
||||||
<li class="ex-admin-nav--menu-item">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ex-admin-nav--menu-item-action"
|
|
||||||
onClick={() => logout()}
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
class="ex-admin-nav--menu-item-icon"
|
|
||||||
icon="fa-solid fa-right-from-bracket"
|
|
||||||
/>
|
|
||||||
<span class="ex-admin-nav--menu-item-title">Logout</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -3,6 +3,7 @@ import { defineNuxtConfig } from 'nuxt/config';
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: [
|
modules: [
|
||||||
'nuxt-graphql-client',
|
'nuxt-graphql-client',
|
||||||
|
'nuxt-icon',
|
||||||
],
|
],
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|
49
packages/example-site/package-lock.json
generated
49
packages/example-site/package-lock.json
generated
|
@ -812,32 +812,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@fortawesome/fontawesome-common-types": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ=="
|
|
||||||
},
|
|
||||||
"@fortawesome/fontawesome-svg-core": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==",
|
|
||||||
"requires": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@fortawesome/free-solid-svg-icons": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==",
|
|
||||||
"requires": {
|
|
||||||
"@fortawesome/fontawesome-common-types": "6.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@fortawesome/vue-fontawesome": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-xHVtVY8ASUeEvgcA/7vULUesENhD+pi/EirRHdMBqooHlXBqK+yrV6d8tUye1m5UKQKVgYAHMhUBfOnoiwvc8Q=="
|
|
||||||
},
|
|
||||||
"@graphql-codegen/cli": {
|
"@graphql-codegen/cli": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.1.tgz",
|
||||||
|
@ -2033,6 +2007,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
|
||||||
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
|
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="
|
||||||
},
|
},
|
||||||
|
"@iconify/types": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
|
||||||
|
},
|
||||||
|
"@iconify/vue": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LRp+mYh8N0bcX4lustHtI5o1aEoio9HN3/19uIzVOvI78qopKBjzsDK5hkEZYDSc6+LKG8hfLxTxpW8CejXGZg==",
|
||||||
|
"requires": {
|
||||||
|
"@iconify/types": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ioredis/commands": {
|
"@ioredis/commands": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||||
|
@ -2165,7 +2152,6 @@
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.0.0.tgz",
|
||||||
"integrity": "sha512-7ZsOLt5s9a0ZleAIzmoD70JwkZf5ti6bDdxl6f8ew7Huxz+ni/oRfTPTX9TrORXsgW5CvDt6Q9M7IJNPkAN/Iw==",
|
"integrity": "sha512-7ZsOLt5s9a0ZleAIzmoD70JwkZf5ti6bDdxl6f8ew7Huxz+ni/oRfTPTX9TrORXsgW5CvDt6Q9M7IJNPkAN/Iw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nuxt/schema": "3.0.0",
|
"@nuxt/schema": "3.0.0",
|
||||||
"c12": "^1.0.1",
|
"c12": "^1.0.1",
|
||||||
|
@ -6713,6 +6699,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nuxt-icon": {
|
||||||
|
"version": "0.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/nuxt-icon/-/nuxt-icon-0.1.8.tgz",
|
||||||
|
"integrity": "sha512-oPFlLOZCy80MN+hf49+mBkOIHWVF3sOqZREQZw3qD0N6wGlR15QeRQtKQC8qGeQcc+xvpLQm0GvrdJ8FxFOPYg==",
|
||||||
|
"requires": {
|
||||||
|
"@iconify/vue": "^4.0.1",
|
||||||
|
"@nuxt/kit": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|
|
@ -13,9 +13,6 @@
|
||||||
"nuxt": "3.0.0"
|
"nuxt": "3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
|
||||||
"@graphql-codegen/cli": "^2.16.1",
|
"@graphql-codegen/cli": "^2.16.1",
|
||||||
"@schlechtenburg/core": "^0.0.0",
|
"@schlechtenburg/core": "^0.0.0",
|
||||||
"@schlechtenburg/heading": "^0.0.0",
|
"@schlechtenburg/heading": "^0.0.0",
|
||||||
|
@ -24,6 +21,7 @@
|
||||||
"@schlechtenburg/paragraph": "^0.0.0",
|
"@schlechtenburg/paragraph": "^0.0.0",
|
||||||
"@schlechtenburg/style": "^0.0.0",
|
"@schlechtenburg/style": "^0.0.0",
|
||||||
"event-target-polyfill": "^0.0.3",
|
"event-target-polyfill": "^0.0.3",
|
||||||
"nuxt-graphql-client": "^0.2.23"
|
"nuxt-graphql-client": "^0.2.23",
|
||||||
|
"nuxt-icon": "^0.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|
||||||
import {
|
|
||||||
faRightFromBracket,
|
|
||||||
faHome,
|
|
||||||
faArrowRight,
|
|
||||||
faArrowLeft,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
|
|
||||||
/* add icons to the library */
|
|
||||||
library.add(faRightFromBracket);
|
|
||||||
library.add(faArrowRight);
|
|
||||||
library.add(faArrowLeft);
|
|
||||||
library.add(faHome);
|
|
||||||
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.component('font-awesome-icon', FontAwesomeIcon);
|
|
||||||
})
|
|
0
packages/italic/lib/ItalicToolUI.tsx
Normal file
0
packages/italic/lib/ItalicToolUI.tsx
Normal file
8
packages/italic/lib/index.ts
Normal file
8
packages/italic/lib/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
|
||||||
|
export const name = 'sb-italic';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name,
|
||||||
|
ui: defineAsyncComponent(() => import('./ui')),
|
||||||
|
};
|
28
packages/italic/lib/ui.tsx
Normal file
28
packages/italic/lib/ui.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import {
|
||||||
|
SbButton,
|
||||||
|
getSelection,
|
||||||
|
getRangeFromSelection,
|
||||||
|
} from '@schlechtenburg/core';
|
||||||
|
import { surround } from './util';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
async setup() {
|
||||||
|
return () => <SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const selection = getSelection();
|
||||||
|
if (!selection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = getRangeFromSelection(selection);
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
surround(range);
|
||||||
|
}}
|
||||||
|
>i</SbButton>;
|
||||||
|
},
|
||||||
|
});
|
25
packages/italic/lib/util.ts
Normal file
25
packages/italic/lib/util.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import {
|
||||||
|
getSelection,
|
||||||
|
getRangeFromSelection,
|
||||||
|
} from '@schlechtenburg/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap range with <i> tag
|
||||||
|
*/
|
||||||
|
export const surround = (range: Range) => {
|
||||||
|
range.surroundContents(document.createElement('i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check selection and set activated state to button if there are <i> tag
|
||||||
|
*/
|
||||||
|
export const checkState = (): boolean => {
|
||||||
|
const selection = getSelection();
|
||||||
|
const range = getRangeFromSelection(selection);
|
||||||
|
|
||||||
|
console.log(range);
|
||||||
|
|
||||||
|
const isActive = document.queryCommandState('italic');
|
||||||
|
|
||||||
|
return isActive;
|
||||||
|
}
|
1334
packages/italic/package-lock.json
generated
Normal file
1334
packages/italic/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
36
packages/italic/package.json
Normal file
36
packages/italic/package.json
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "@schlechtenburg/italic",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "> TODO: description",
|
||||||
|
"author": "Benjamin Bädorf <hello@benjaminbaedorf.eu>",
|
||||||
|
"homepage": "",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"main": "lib/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "vuedx-typecheck --no-pretty ./lib",
|
||||||
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "__tests__"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"docs"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@git.b12f.io:b12f/schlechtenburg.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@schlechtenburg/core": "^0.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vuedx/typecheck": "^0.6.3",
|
||||||
|
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
||||||
|
"vue": "^3.2.31"
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ import {
|
||||||
SbToolbar,
|
SbToolbar,
|
||||||
SbSelect,
|
SbSelect,
|
||||||
generateBlockId,
|
generateBlockId,
|
||||||
|
SbContenteditable,
|
||||||
|
getSelection,
|
||||||
} from '@schlechtenburg/core';
|
} from '@schlechtenburg/core';
|
||||||
import { isEmptyContentEditable } from './contenteditable';
|
import { isEmptyContentEditable } from './contenteditable';
|
||||||
import {
|
import {
|
||||||
|
@ -77,21 +79,11 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
focusInput();
|
|
||||||
if (inputEl.value) {
|
|
||||||
inputEl.value.innerHTML = localData.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(isActive, focusInput);
|
watch(isActive, focusInput);
|
||||||
|
|
||||||
watch(() => props.data, () => {
|
watch(() => props.data, () => {
|
||||||
localData.value = props.data.value;
|
localData.value = props.data.value;
|
||||||
localData.align = props.data.align;
|
localData.align = props.data.align;
|
||||||
if (inputEl.value) {
|
|
||||||
inputEl.value.innerHTML = localData.value;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onTextUpdate = ($event: Event) => {
|
const onTextUpdate = ($event: Event) => {
|
||||||
|
@ -136,7 +128,7 @@ export default defineComponent({
|
||||||
activate(id);
|
activate(id);
|
||||||
|
|
||||||
$event.preventDefault();
|
$event.preventDefault();
|
||||||
}
|
}window
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyup = ($event: KeyboardEvent) => {
|
const onKeyup = ($event: KeyboardEvent) => {
|
||||||
|
@ -144,7 +136,7 @@ export default defineComponent({
|
||||||
props.eventRemoveSelf();
|
props.eventRemoveSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
const selection = window.getSelection();
|
const selection = getSelection();
|
||||||
const node = selection?.focusNode;
|
const node = selection?.focusNode;
|
||||||
const childNodes = Array.from(inputEl?.value?.childNodes || []);
|
const childNodes = Array.from(inputEl?.value?.childNodes || []);
|
||||||
const index = node ? childNodes.indexOf(node as ChildNode) : -1;
|
const index = node ? childNodes.indexOf(node as ChildNode) : -1;
|
||||||
|
@ -174,16 +166,22 @@ export default defineComponent({
|
||||||
<option>right</option>
|
<option>right</option>
|
||||||
</SbSelect>
|
</SbSelect>
|
||||||
</SbToolbar>
|
</SbToolbar>
|
||||||
<p
|
<SbContenteditable
|
||||||
|
tag="p"
|
||||||
class="sb-paragraph__input"
|
class="sb-paragraph__input"
|
||||||
ref={inputEl}
|
|
||||||
contenteditable
|
inputRef={inputEl}
|
||||||
onInput={onTextUpdate}
|
|
||||||
|
value={localData.value}
|
||||||
|
onValueChange={(value: string) => {
|
||||||
|
localData.value = value;
|
||||||
|
}}
|
||||||
|
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeydown={onKeydown}
|
onKeydown={onKeydown}
|
||||||
onKeyup={onKeyup}
|
onKeyup={onKeyup}
|
||||||
></p>
|
></SbContenteditable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue