Skip to content

Commit

Permalink
Add basic text formatting toolbar buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
raimohanska committed Mar 17, 2024
1 parent 3a48d6f commit d4f7cf5
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 2 deletions.
3 changes: 2 additions & 1 deletion common/src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/board/CollaborativeTextView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { QuillBinding } from "y-quill"
import {
AccessLevel,
Board,
LocalUIEvent,
TextItem,
canWrite,
getAlign,
Expand Down Expand Up @@ -33,6 +34,7 @@ interface CollaborativeTextViewProps {
itemFocus: L.Property<"none" | "selected" | "dragging" | "editing">
crdtStore: CRDTStore
isLocked: L.Property<boolean>
uiEvents: L.EventStream<LocalUIEvent>
}
export function CollaborativeTextView({
id,
Expand All @@ -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)
Expand Down Expand Up @@ -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 (
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/board/ItemView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h } from "harmaja"
import { componentScope, h } from "harmaja"
import * as L from "lonna"
import {
AccessLevel,
Expand All @@ -10,6 +10,7 @@ import {
getItemShape,
getVerticalAlign,
isContainer,
isLocalUIEvent,
isTextItem,
Item,
ItemType,
Expand Down Expand Up @@ -148,6 +149,7 @@ export const ItemView = ({
itemFocus={itemFocus}
crdtStore={boardStore.crdtStore}
isLocked={isLocked}
uiEvents={boardStore.events.pipe(L.filter(isLocalUIEvent), L.applyScope(componentScope()))}
/>
) : (
<TextView
Expand Down
1 change: 1 addition & 0 deletions frontend/src/board/board-permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Connection, Item } from "../../../common/src/domain"
export const canChangeFont: BoardPermission = (item) => !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
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/board/contextmenu/ContextMenuView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] }>
Expand Down Expand Up @@ -100,6 +101,7 @@ export const ContextMenuView = ({
alignmentsMenu("y", props),
colorsAndShapesMenu(props),
fontSizesMenu(props),
textFormatsMenu(props),
textAlignmentsMenu(props),
areaTilingMenu(props),
connectionEndsMenu(props),
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/board/contextmenu/textFormats.tsx
Original file line number Diff line number Diff line change
@@ -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
? []
: [
<div className="icon-group text-format">
<span
className={className}
onClick={() => {
dispatch({
action: "ui.text.format",
itemIds: textItems.get().map((i) => i.id),
format: "bold",
})
}}
title="Bold"
>
<BoldIcon />
</span>
<span
className={className}
onClick={() => {
dispatch({
action: "ui.text.format",
itemIds: textItems.get().map((i) => i.id),
format: "italic",
})
}}
title="Italic"
>
<ItalicIcon />
</span>
<span
className={className}
onClick={() => {
dispatch({
action: "ui.text.format",
itemIds: textItems.get().map((i) => i.id),
format: "underline",
})
}}
title="Underline"
>
<UnderlineIcon />
</span>
</div>,
,
]
})
}
24 changes: 24 additions & 0 deletions frontend/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,30 @@ export const TileIcon = () => (
</svg>
)

export const BoldIcon = () => (
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42M10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5"
></path>
</svg>
)

export const ItalicIcon = () => (
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"></path>
</svg>
)

export const UnderlineIcon = () => (
<svg viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6m-7 2v2h14v-2z"
></path>
</svg>
)

export const BackIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20v-2z" />
Expand Down

0 comments on commit d4f7cf5

Please sign in to comment.