diff --git a/common/src/domain.ts b/common/src/domain.ts index b7c950d3..d7f66688 100644 --- a/common/src/domain.ts +++ b/common/src/domain.ts @@ -210,7 +210,7 @@ export type RecentBoard = RecentBoardAttributes & { opened: ISOTimeStamp; userEm export type BoardEvent = { boardId: Id } export type UIEvent = BoardItemEvent | ClientToServerRequest | LocalUIEvent -export type LocalUIEvent = Undo | Redo | SetLocalBoard | GoOnline | BoardLoggedOut | GoOffline +export type LocalUIEvent = Undo | Redo | SetLocalBoard | GoOnline | BoardLoggedOut | GoOffline | TextFormat export type EventFromServer = BoardHistoryEntry | BoardStateSyncEvent | LoginResponse | AckAddBoard | ServerConfig export type ServerConfig = { action: "server.config" @@ -364,6 +364,7 @@ export type AssetPutUrlRequest = { action: "asset.put.request"; assetId: string export type AssetPutUrlResponse = { action: "asset.put.response"; assetId: string; signedUrl: string } export type Undo = { action: "ui.undo" } export type Redo = { action: "ui.redo" } +export type TextFormat = { action: "ui.text.format"; itemIds: Id[]; format: "bold" | "italic" | "underline" } export type SetLocalBoard = { action: "ui.board.setLocal" diff --git a/frontend/src/board/CollaborativeTextView.tsx b/frontend/src/board/CollaborativeTextView.tsx index f0432d2a..ce11f6ef 100644 --- a/frontend/src/board/CollaborativeTextView.tsx +++ b/frontend/src/board/CollaborativeTextView.tsx @@ -6,6 +6,7 @@ import { QuillBinding } from "y-quill" import { AccessLevel, Board, + LocalUIEvent, TextItem, canWrite, getAlign, @@ -33,6 +34,7 @@ interface CollaborativeTextViewProps { itemFocus: L.Property<"none" | "selected" | "dragging" | "editing"> crdtStore: CRDTStore isLocked: L.Property + uiEvents: L.EventStream } export function CollaborativeTextView({ id, @@ -43,6 +45,7 @@ export function CollaborativeTextView({ itemFocus, isLocked, crdtStore, + uiEvents, }: CollaborativeTextViewProps) { const fontSize = L.view(item, (i) => `${i.fontSize ? i.fontSize : 1}em`) const color = L.view(item, getItemBackground, contrastingColor) @@ -108,6 +111,16 @@ export function CollaborativeTextView({ quillEditor.get()?.formatText(0, 10000000, "align", align === "left" ? "" : align) }) + uiEvents.applyScope(componentScope()).forEach((e) => { + if (e.action === "ui.text.format" && e.itemIds.includes(id)) { + const quill = quillEditor.get() + const selection = quill?.getSelection() + const format = selection && quill?.getFormat(selection) + const newValue = !(format && format[e.format]) + quill?.format(e.format, newValue) + } + }) + let touchMoves = 0 return ( diff --git a/frontend/src/board/ItemView.tsx b/frontend/src/board/ItemView.tsx index 1f7adef9..b3a83974 100644 --- a/frontend/src/board/ItemView.tsx +++ b/frontend/src/board/ItemView.tsx @@ -1,4 +1,4 @@ -import { h } from "harmaja" +import { componentScope, h } from "harmaja" import * as L from "lonna" import { AccessLevel, @@ -10,6 +10,7 @@ import { getItemShape, getVerticalAlign, isContainer, + isLocalUIEvent, isTextItem, Item, ItemType, @@ -148,6 +149,7 @@ export const ItemView = ({ itemFocus={itemFocus} crdtStore={boardStore.crdtStore} isLocked={isLocked} + uiEvents={boardStore.events.pipe(L.filter(isLocalUIEvent), L.applyScope(componentScope()))} /> ) : ( !item.locked export const canChangeShapeAndColor: BoardPermission = (item): boolean => !item.locked export const canChangeTextAlign: BoardPermission = (item): boolean => !item.locked +export const canChangeTextFormat: BoardPermission = (item): boolean => !item.locked export const canChangeVisibility: BoardPermission = (item): boolean => !item.locked export const canChangeText: BoardPermission = (item): boolean => true export const canMove: BoardPermission = (item): boolean => !item.locked diff --git a/frontend/src/board/contextmenu/ContextMenuView.tsx b/frontend/src/board/contextmenu/ContextMenuView.tsx index b81c64d0..06dcc8cc 100644 --- a/frontend/src/board/contextmenu/ContextMenuView.tsx +++ b/frontend/src/board/contextmenu/ContextMenuView.tsx @@ -14,6 +14,7 @@ import { connectionEndsMenu } from "./connection-ends" import { textAlignmentsMenu } from "./textAlignments" import { lockMenu } from "./lock" import { hideContentsMenu } from "./hideContents" +import { textFormatsMenu } from "./textFormats" export type SubmenuProps = { focusedItems: L.Property<{ items: Item[]; connections: Connection[] }> @@ -100,6 +101,7 @@ export const ContextMenuView = ({ alignmentsMenu("y", props), colorsAndShapesMenu(props), fontSizesMenu(props), + textFormatsMenu(props), textAlignmentsMenu(props), areaTilingMenu(props), connectionEndsMenu(props), diff --git a/frontend/src/board/contextmenu/textFormats.tsx b/frontend/src/board/contextmenu/textFormats.tsx new file mode 100644 index 00000000..cd82f619 --- /dev/null +++ b/frontend/src/board/contextmenu/textFormats.tsx @@ -0,0 +1,66 @@ +import { h } from "harmaja" +import * as L from "lonna" +import { CrdtEnabled, isTextItem } from "../../../../common/src/domain" +import { BoldIcon, ItalicIcon, UnderlineIcon } from "../../components/Icons" +import { canChangeTextFormat } from "../board-permissions" +import { SubmenuProps } from "./ContextMenuView" + +export function textFormatsMenu({ board, focusedItems, dispatch }: SubmenuProps) { + const textItems = L.view(focusedItems, (items) => + items.items.filter((i) => isTextItem(i) && i.crdt === CrdtEnabled), + ) + const singleText = L.view(focusedItems, textItems, (f, t) => f.items.length === 1 && t.length === f.items.length) + + const enabled = L.view(textItems, (items) => items.some(canChangeTextFormat)) + + const className = enabled.pipe(L.map((e) => (e ? "icon" : "icon disabled"))) + + return L.view(singleText, (singleText) => { + return !singleText + ? [] + : [ +
+ { + dispatch({ + action: "ui.text.format", + itemIds: textItems.get().map((i) => i.id), + format: "bold", + }) + }} + title="Bold" + > + + + { + dispatch({ + action: "ui.text.format", + itemIds: textItems.get().map((i) => i.id), + format: "italic", + }) + }} + title="Italic" + > + + + { + dispatch({ + action: "ui.text.format", + itemIds: textItems.get().map((i) => i.id), + format: "underline", + }) + }} + title="Underline" + > + + +
, + , + ] + }) +} diff --git a/frontend/src/components/Icons.tsx b/frontend/src/components/Icons.tsx index 2c89cf7c..3e6d7615 100644 --- a/frontend/src/components/Icons.tsx +++ b/frontend/src/components/Icons.tsx @@ -266,6 +266,30 @@ export const TileIcon = () => ( ) +export const BoldIcon = () => ( + + + +) + +export const ItalicIcon = () => ( + + + +) + +export const UnderlineIcon = () => ( + + + +) + export const BackIcon = () => (