Start work on inline tools

This commit is contained in:
Benjamin Bädorf 2022-12-30 00:33:04 +01:00
parent e3ddcefb30
commit 0fcb0d03a0
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
33 changed files with 2013 additions and 169 deletions

View file

@ -21,3 +21,7 @@ export default {
view: defineAsyncComponent(() => import('./view')),
} 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.

View file

@ -164,6 +164,7 @@ export const SbBlock = defineComponent({
eventRemoveSelf={props.eventRemoveSelf}
eventActivatePrevious={props.eventActivatePrevious}
eventActivateNext={props.eventActivateNext}
data-sb-block--content
{...{
onClick: ($event: MouseEvent) => {

View 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 || '');
},
});
},
});

View file

@ -2,6 +2,7 @@ import {
defineComponent,
provide,
shallowReactive,
shallowRef,
ref,
watch,
computed,
@ -15,6 +16,8 @@ import {
IBlockLibrary,
ITreeNode,
OnUpdateBlockCb,
IInlineToolDefinition,
IInlineToolLibrary,
} from '../types';
import { model } from '../block-helpers';
import { SymMode, SbMode } from '../mode';
@ -26,12 +29,19 @@ import {
} from '../use-block-tree';
import { SymEditorDimensions, useResizeObserver } from '../use-resize-observer';
import { SymActiveBlock } from '../use-activation';
import { SymRootElement } from '../use-root-element';
import {
SbInlineToolbar,
SymInlineToolbarClients,
SymInlineToolLibrary,
} from '../inline';
import { SbMainMenu } from './MainMenu';
import { SbBlock } from './Block';
export interface ISbMainProps {
availableBlocks: IBlockDefinition<any>[];
availableInlineTools: IInlineToolDefinition[];
block: IBlockData<any>;
eventUpdate: OnUpdateBlockCb;
mode: SbMode;
@ -49,6 +59,10 @@ export const SbMain = defineComponent({
type: Array as PropType<IBlockDefinition<any>[]>,
default: () => [],
},
availableInlineTools: {
type: Array as PropType<IInlineToolDefinition[]>,
default: () => [],
},
block: {
type: Object as PropType<IBlockData<any>>,
required: true,
@ -72,6 +86,10 @@ export const SbMain = defineComponent({
setup(props: ISbMainProps) {
const el: Ref<null|HTMLElement> = ref(null);
useResizeObserver(el, SymEditorDimensions);
provide(SymRootElement, el);
const inlineClients = shallowRef([]);
provide(SymInlineToolbarClients, inlineClients);
const mode = ref(props.mode);
provide(SymMode, mode);
@ -99,9 +117,16 @@ export const SbMain = defineComponent({
{},
),
});
provide(SymBlockLibrary, blockLibrary);
const inlineToolLibrary: IInlineToolLibrary = shallowReactive({
...props.availableInlineTools.reduce(
(tools: IInlineToolLibrary, tool: IInlineToolDefinition) => ({ ...tools, [tool.name]: tool }),
{},
),
});
provide(SymInlineToolLibrary, inlineToolLibrary);
return () => (
<div
class={classes.value}
@ -123,6 +148,7 @@ export const SbMain = defineComponent({
block={props.block}
eventUpdate={props.eventUpdate}
/>
<SbInlineToolbar />
</div>
);
},

View file

@ -1,6 +1,8 @@
export * from './mode';
export * from './types';
export * from './inline';
export * from './block-helpers';
export * from './use-activation';
@ -8,10 +10,14 @@ export * from './use-dynamic-blocks';
export * from './use-resize-observer';
export * from './components/Main';
export * from './components/Block';
export * from './components/BlockPicker';
export * from './components/BlockOrdering';
export * from './components/BlockPlaceholder';
export * from './components/Contenteditable';
export * from './components/Toolbar';
export * from './components/Button';
export * from './components/Select';

View 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;
}
}

View 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;
},
});

View 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;
}

View file

@ -0,0 +1,4 @@
export * from './InlineToolbar';
export * from './use-inline';
export * from './selection';
export * from './dom';

View 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;
};

View 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,
};
};

View file

@ -170,3 +170,22 @@ export interface IBlockDefinition<T> {
export interface IBlockLibrary {
[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;
}

View file

@ -7,7 +7,7 @@ import {
} from 'vue';
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 isActive = computed(() => activeBlockId.value === currentBlockId);
@ -46,4 +46,4 @@ export function useActivation(currentBlockId: string|null = null) {
deactivate,
requestActivation,
};
}
};

View file

@ -10,14 +10,12 @@ import {
ITreeNode,
IBlockData,
} from './types';
import { useDynamicBlocks } from './use-dynamic-blocks';
import { SbMode } from './mode';
export const SymBlockTree= Symbol('Schlechtenburg block tree');
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregister');
export function useBlockTree() {
export const useBlockTree = () => {
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
@ -52,8 +50,6 @@ export function useBlockTree() {
self.children = self.children.filter((child: ITreeNode) => child.id !== id);
});
const { mode } = useDynamicBlocks();
const register = (block: IBlockData<any>, index: number = 0) => {
if (!block.id) {
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
@ -77,4 +73,4 @@ export function useBlockTree() {
blockTree,
register,
};
}
};

View file

@ -16,7 +16,7 @@ interface BlockRect {
export const SymBlockDimensions = Symbol('Schlechtenburg block 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);
provide(symbol, dimensions);
const triggerSizeCalculation = () => {
@ -47,7 +47,7 @@ export function useResizeObserver(el: Ref<null|HTMLElement>, symbol: symbol) {
})
return { triggerSizeCalculation, dimensions };
}
};
export function useBlockSizing() {
const editorDimensions: Ref<BlockRect|null> = inject(SymEditorDimensions, ref(null));

View 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 };
};

View file

@ -43,7 +43,7 @@ body {
box-sizing: border-box;
}
&--admin-nav {
&--edit-nav {
z-index: 100;
position: fixed;
top: 0;
@ -62,7 +62,7 @@ body {
@media screen and (min-width: 1000px) {
--ex-nav-width: var(--ex-nav-expanded-width);
&--admin-nav {
&--edit-nav {
position: unset;
width: unset;
flex-basis: var(--ex-nav-width);

View file

@ -3,14 +3,14 @@ import { NuxtPage } from '#components';
import './app.scss';
const AdminNav = defineAsyncComponent(() => import('~~/components/admin/Nav'));
const AdminNav = defineAsyncComponent(() => import('~~/components/_/Nav'));
export default defineComponent({
setup() {
const { me } = useMe();
return () => (
<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" />
</div>
);

View file

@ -1,10 +1,14 @@
.ex-admin-nav {
.ex-edit-nav {
background-color: white;
width: var(--ex-nav-width);
display: flex;
flex-direction: column;
align-items: stretch;
transition: width 0.2s ease;
.icon {
height: 1.5rem;
width: 1.5rem;
}
&_expanded {
width: 300px;
@ -54,24 +58,17 @@
width: calc(100% - 60px);
overflow: hidden;
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 {
&-title {
//width: calc(100% - (32px + 24px));
opacity: 1;
&-action {
justify-content: flex-start;
}
&-icon {
&-title {
opacity: 1;
margin-left: 16px;
}
}

View 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>
);
},
});

View file

@ -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>
);
},
});

View file

@ -3,6 +3,7 @@ import { defineNuxtConfig } from 'nuxt/config';
export default defineNuxtConfig({
modules: [
'nuxt-graphql-client',
'nuxt-icon',
],
runtimeConfig: {

View file

@ -812,32 +812,6 @@
"dev": 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": {
"version": "2.16.1",
"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",
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@ -2165,7 +2152,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.0.0.tgz",
"integrity": "sha512-7ZsOLt5s9a0ZleAIzmoD70JwkZf5ti6bDdxl6f8ew7Huxz+ni/oRfTPTX9TrORXsgW5CvDt6Q9M7IJNPkAN/Iw==",
"dev": true,
"requires": {
"@nuxt/schema": "3.0.0",
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",

View file

@ -13,9 +13,6 @@
"nuxt": "3.0.0"
},
"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",
"@schlechtenburg/core": "^0.0.0",
"@schlechtenburg/heading": "^0.0.0",
@ -24,6 +21,7 @@
"@schlechtenburg/paragraph": "^0.0.0",
"@schlechtenburg/style": "^0.0.0",
"event-target-polyfill": "^0.0.3",
"nuxt-graphql-client": "^0.2.23"
"nuxt-graphql-client": "^0.2.23",
"nuxt-icon": "^0.1.8"
}
}

View file

@ -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);
})

View file

View file

@ -0,0 +1,8 @@
import { defineAsyncComponent } from 'vue';
export const name = 'sb-italic';
export default {
name,
ui: defineAsyncComponent(() => import('./ui')),
};

View 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>;
},
});

View 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

File diff suppressed because it is too large Load diff

View 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"
}
}

View file

@ -14,6 +14,8 @@ import {
SbToolbar,
SbSelect,
generateBlockId,
SbContenteditable,
getSelection,
} from '@schlechtenburg/core';
import { isEmptyContentEditable } from './contenteditable';
import {
@ -77,21 +79,11 @@ export default defineComponent({
}
};
onMounted(() => {
focusInput();
if (inputEl.value) {
inputEl.value.innerHTML = localData.value;
}
});
watch(isActive, focusInput);
watch(() => props.data, () => {
localData.value = props.data.value;
localData.align = props.data.align;
if (inputEl.value) {
inputEl.value.innerHTML = localData.value;
}
});
const onTextUpdate = ($event: Event) => {
@ -136,7 +128,7 @@ export default defineComponent({
activate(id);
$event.preventDefault();
}
}window
};
const onKeyup = ($event: KeyboardEvent) => {
@ -144,7 +136,7 @@ export default defineComponent({
props.eventRemoveSelf();
}
const selection = window.getSelection();
const selection = getSelection();
const node = selection?.focusNode;
const childNodes = Array.from(inputEl?.value?.childNodes || []);
const index = node ? childNodes.indexOf(node as ChildNode) : -1;
@ -174,16 +166,22 @@ export default defineComponent({
<option>right</option>
</SbSelect>
</SbToolbar>
<p
<SbContenteditable
tag="p"
class="sb-paragraph__input"
ref={inputEl}
contenteditable
onInput={onTextUpdate}
inputRef={inputEl}
value={localData.value}
onValueChange={(value: string) => {
localData.value = value;
}}
onFocus={onFocus}
onBlur={onBlur}
onKeydown={onKeydown}
onKeyup={onKeyup}
></p>
></SbContenteditable>
</div>
);
},