Implemented better page numbering (#779)
* Implemented better page numbering * Added spanish and french translation * Add tests, fix regex * Improve page naming logic Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
1de57cffc0
commit
4d900fb7fd
13 changed files with 184 additions and 69 deletions
|
@ -13,6 +13,7 @@ export const defaultDocument: TDDocument = {
|
||||||
bindings: {},
|
bindings: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assets: {},
|
||||||
pageStates: {
|
pageStates: {
|
||||||
page: {
|
page: {
|
||||||
id: 'page',
|
id: 'page',
|
||||||
|
|
|
@ -1485,24 +1485,6 @@ left past the initial left edge) then swap points on that axis.
|
||||||
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
|
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
|
||||||
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
|
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an incremented name (e.g. New page (2)) from a name (e.g. New page), based on an array of existing names.
|
|
||||||
*
|
|
||||||
* @param name The name to increment.
|
|
||||||
* @param others The array of existing names.
|
|
||||||
*/
|
|
||||||
static getIncrementedName(name: string, others: string[]) {
|
|
||||||
let result = name
|
|
||||||
|
|
||||||
while (others.includes(result)) {
|
|
||||||
result = /\s\((\d+)\)$/.exec(result)?.[1]
|
|
||||||
? result.replace(/\d+(?=\)$)/, (m) => (+m + 1).toString())
|
|
||||||
: `${result} (1)`
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Utils
|
export default Utils
|
||||||
|
|
|
@ -66,7 +66,9 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
||||||
const currentPageId = app.useStore(currentPageIdSelector)
|
const currentPageId = app.useStore(currentPageIdSelector)
|
||||||
|
|
||||||
const handleCreatePage = React.useCallback(() => {
|
const handleCreatePage = React.useCallback(() => {
|
||||||
app.createPage(undefined, intl.formatMessage({ id: 'new.page' }))
|
const pageName =
|
||||||
|
intl.formatMessage({ id: 'page' }) + ' ' + (Object.keys(app.document.pages).length + 1)
|
||||||
|
app.createPage(undefined, pageName)
|
||||||
}, [app])
|
}, [app])
|
||||||
|
|
||||||
const handleChangePage = React.useCallback(
|
const handleChangePage = React.useCallback(
|
||||||
|
|
|
@ -1,19 +1,10 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { TRANSLATIONS, TDLanguage } from '../translations/translations'
|
import { getTranslation, TDLanguage } from '../translations/translations'
|
||||||
|
|
||||||
export function useTranslation(code?: TDLanguage) {
|
export function useTranslation(code?: TDLanguage) {
|
||||||
return React.useMemo(() => {
|
return React.useMemo(() => {
|
||||||
const locale = code ?? navigator.language.split(/[-_]/)[0]
|
const locale = code ?? navigator.language.split(/[-_]/)[0]
|
||||||
|
|
||||||
const translation = TRANSLATIONS.find((t) => t.code === locale)
|
return getTranslation(locale)
|
||||||
|
|
||||||
const defaultTranslation = TRANSLATIONS.find((t) => t.code === 'en')!
|
|
||||||
|
|
||||||
const messages = {
|
|
||||||
...defaultTranslation.messages,
|
|
||||||
...translation?.messages,
|
|
||||||
}
|
|
||||||
|
|
||||||
return { locale, messages }
|
|
||||||
}, [code])
|
}, [code])
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@ import { StateManager } from './StateManager'
|
||||||
import { clearPrevSize } from './shapes/shared/getTextSize'
|
import { clearPrevSize } from './shapes/shared/getTextSize'
|
||||||
import { getClipboard, setClipboard } from './IdbClipboard'
|
import { getClipboard, setClipboard } from './IdbClipboard'
|
||||||
import { deepCopy } from './StateManager/copy'
|
import { deepCopy } from './StateManager/copy'
|
||||||
|
import { getTranslation } from '~translations'
|
||||||
|
|
||||||
const uuid = Utils.uniqueId()
|
const uuid = Utils.uniqueId()
|
||||||
|
|
||||||
|
@ -1226,9 +1227,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
this.pasteInfo.offset = [0, 0]
|
this.pasteInfo.offset = [0, 0]
|
||||||
this.currentTool = this.tools.select
|
this.currentTool = this.tools.select
|
||||||
|
|
||||||
|
const doc = TldrawApp.defaultDocument
|
||||||
|
|
||||||
|
// Set the default page name to the localized version of "Page"
|
||||||
|
const translation = getTranslation(this.settings.language)
|
||||||
|
doc.pages['page'].name = translation.messages['page'] + ' 1' ?? 'Page 1'
|
||||||
|
|
||||||
this.resetHistory()
|
this.resetHistory()
|
||||||
.clearSelectHistory()
|
.clearSelectHistory()
|
||||||
.loadDocument(migrate(TldrawApp.defaultDocument, TldrawApp.version))
|
.loadDocument(migrate(doc, TldrawApp.version))
|
||||||
.persist({})
|
.persist({})
|
||||||
|
|
||||||
return this
|
return this
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
import { mockDocument, TldrawTestApp } from '~test'
|
import { mockDocument, TldrawTestApp } from '~test'
|
||||||
|
|
||||||
describe('Create page command', () => {
|
let app: TldrawTestApp
|
||||||
const app = new TldrawTestApp()
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app = new TldrawTestApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
function createPageWithName(app: TldrawTestApp) {
|
||||||
|
const pageName = 'Page' + ' ' + (Object.keys(app.document.pages).length + 1)
|
||||||
|
app.createPage(undefined, pageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Create page command', () => {
|
||||||
it('does, undoes and redoes command', () => {
|
it('does, undoes and redoes command', () => {
|
||||||
app.loadDocument(mockDocument)
|
app.loadDocument(mockDocument)
|
||||||
|
|
||||||
|
@ -34,30 +43,116 @@ describe('Create page command', () => {
|
||||||
it('increments page names', () => {
|
it('increments page names', () => {
|
||||||
app.loadDocument(mockDocument)
|
app.loadDocument(mockDocument)
|
||||||
|
|
||||||
app.createPage()
|
createPageWithName(app)
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page')
|
expect(app.page.name).toBe('Page 2')
|
||||||
|
|
||||||
app.createPage()
|
createPageWithName(app)
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page (1)')
|
expect(app.page.name).toBe('Page 3')
|
||||||
|
|
||||||
app.createPage()
|
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page (2)')
|
|
||||||
|
|
||||||
app.renamePage(app.page.id, 'New page!')
|
|
||||||
|
|
||||||
app.createPage()
|
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page (2)')
|
|
||||||
|
|
||||||
app.deletePage(app.page.id)
|
app.deletePage(app.page.id)
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page!')
|
createPageWithName(app)
|
||||||
|
|
||||||
app.createPage(undefined, 'New page!')
|
expect(app.page.name).toBe('Page 3')
|
||||||
|
|
||||||
expect(app.page.name).toBe('New page! (1)')
|
createPageWithName(app)
|
||||||
|
|
||||||
|
expect(app.page.name).toBe('Page 4')
|
||||||
|
|
||||||
|
app.renamePage(app.page.id, 'Page!')
|
||||||
|
|
||||||
|
createPageWithName(app)
|
||||||
|
|
||||||
|
expect(app.page.name).toBe('Page 5')
|
||||||
|
|
||||||
|
app.renamePage(app.page.id, 'Page 6')
|
||||||
|
|
||||||
|
createPageWithName(app)
|
||||||
|
|
||||||
|
expect(app.page.name).toBe('Page 7')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the page name exists', () => {
|
||||||
|
it('when others is empty', () => {
|
||||||
|
app.loadDocument(mockDocument)
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has no match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has one match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has two matches', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple 1')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has a near match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple ()')
|
||||||
|
app.createPage(undefined, 'Apples')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has a near match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple 1!')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has a near match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple 1!')
|
||||||
|
app.createPage(undefined, 'Apple 1!')
|
||||||
|
expect(app.page.name).toBe('Apple 1! 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has a near match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple 1')
|
||||||
|
app.createPage(undefined, 'Apple 2')
|
||||||
|
app.createPage(undefined, 'Apple 3')
|
||||||
|
app.createPage(undefined, 'Apple 5')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when others has a near match', () => {
|
||||||
|
app.createPage(undefined, 'Orange')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
app.createPage(undefined, 'Apple 1')
|
||||||
|
app.createPage(undefined, 'Apple 2')
|
||||||
|
app.createPage(undefined, 'Apple 3')
|
||||||
|
app.createPage(undefined, 'Apple 4')
|
||||||
|
app.createPage(undefined, 'Apple 5')
|
||||||
|
app.createPage(undefined, 'Apple 6')
|
||||||
|
app.createPage(undefined, 'Apple 7')
|
||||||
|
app.createPage(undefined, 'Apple 8')
|
||||||
|
app.createPage(undefined, 'Apple 9')
|
||||||
|
app.createPage(undefined, 'Apple 10')
|
||||||
|
app.createPage(undefined, 'Apple')
|
||||||
|
expect(app.page.name).toBe('Apple 11')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import type { TldrawCommand, TDPage } from '~types'
|
import type { TldrawCommand, TDPage } from '~types'
|
||||||
import { Utils, TLPageState } from '@tldraw/core'
|
import { Utils, TLPageState } from '@tldraw/core'
|
||||||
import type { TldrawApp } from '~state'
|
import type { TldrawApp } from '~state'
|
||||||
|
import { getIncrementedName } from '../shared/getIncrementedName'
|
||||||
|
|
||||||
export function createPage(
|
export function createPage(
|
||||||
app: TldrawApp,
|
app: TldrawApp,
|
||||||
center: number[],
|
center: number[],
|
||||||
pageId = Utils.uniqueId(),
|
pageId = Utils.uniqueId(),
|
||||||
pageName = 'New page'
|
pageName = 'Page'
|
||||||
): TldrawCommand {
|
): TldrawCommand {
|
||||||
const { currentPageId } = app
|
const { currentPageId } = app
|
||||||
|
|
||||||
|
@ -20,7 +21,7 @@ export function createPage(
|
||||||
|
|
||||||
const page: TDPage = {
|
const page: TDPage = {
|
||||||
id: pageId,
|
id: pageId,
|
||||||
name: Utils.getIncrementedName(
|
name: getIncrementedName(
|
||||||
pageName,
|
pageName,
|
||||||
pages.map((p) => p.name ?? '')
|
pages.map((p) => p.name ?? '')
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Get an incremented name (e.g. Page 2) from a name (e.g. Page 1), based on an array of existing names.
|
||||||
|
*
|
||||||
|
* @param name The name to increment.
|
||||||
|
* @param others The array of existing names.
|
||||||
|
*/
|
||||||
|
export function getIncrementedName(name: string, others: string[]) {
|
||||||
|
let result = name
|
||||||
|
const set = new Set(others)
|
||||||
|
|
||||||
|
while (set.has(result)) {
|
||||||
|
result = /^.*(\d+)$/.exec(result)?.[1]
|
||||||
|
? result.replace(/(\d+)(?=\D?)$/, (m) => (+m + 1).toString())
|
||||||
|
: `${result} 1`
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -51,6 +51,7 @@
|
||||||
"create.page": "Crear página",
|
"create.page": "Crear página",
|
||||||
"new.page": "Nueva página",
|
"new.page": "Nueva página",
|
||||||
"page.name": "Nombre de página",
|
"page.name": "Nombre de página",
|
||||||
|
"page": "Página",
|
||||||
"duplicate": "Duplicar",
|
"duplicate": "Duplicar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"copy.invite.link": "Copiar invitación",
|
"copy.invite.link": "Copiar invitación",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"create.page": "Créer une Page",
|
"create.page": "Créer une Page",
|
||||||
"new.page": "Nouvelle Page",
|
"new.page": "Nouvelle Page",
|
||||||
"page.name": "Nom de la Page",
|
"page.name": "Nom de la Page",
|
||||||
|
"page": "Page",
|
||||||
"duplicate": "Dupliquer",
|
"duplicate": "Dupliquer",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"copy.invite.link": "Copier le Lien d'Invitation",
|
"copy.invite.link": "Copier le Lien d'Invitation",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"create.page": "Crea nuova pagina",
|
"create.page": "Crea nuova pagina",
|
||||||
"new.page": "Nuova pagina",
|
"new.page": "Nuova pagina",
|
||||||
"page.name": "Nome pagina",
|
"page.name": "Nome pagina",
|
||||||
|
"page": "Pagina",
|
||||||
"duplicate": "Duplica",
|
"duplicate": "Duplica",
|
||||||
"cancel": "Chiudi",
|
"cancel": "Chiudi",
|
||||||
"copy.invite.link": "Copia link invito",
|
"copy.invite.link": "Copia link invito",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"create.page": "Create Page",
|
"create.page": "Create Page",
|
||||||
"new.page": "New Page",
|
"new.page": "New Page",
|
||||||
"page.name": "Page Name",
|
"page.name": "Page Name",
|
||||||
|
"page": "Page",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"copy.invite.link": "Copy Invite Link",
|
"copy.invite.link": "Copy Invite Link",
|
||||||
|
|
|
@ -22,23 +22,23 @@ import zh_cn from './zh-cn.json'
|
||||||
// translation instead.
|
// translation instead.
|
||||||
|
|
||||||
export const TRANSLATIONS: TDTranslations = [
|
export const TRANSLATIONS: TDTranslations = [
|
||||||
{ code: 'ar', label: 'عربي', messages: ar },
|
{ code: 'ar', locale: 'ar', label: 'عربي', messages: ar },
|
||||||
{ code: 'da', label: 'Danish', messages: da },
|
{ code: 'en', locale: 'en', label: 'English', messages: en },
|
||||||
{ code: 'de', label: 'Deutsch', messages: de },
|
{ code: 'es', locale: 'es', label: 'Español', messages: es },
|
||||||
{ code: 'en', label: 'English', messages: en },
|
{ code: 'fr', locale: 'fr', label: 'Français', messages: fr },
|
||||||
{ code: 'es', label: 'Español', messages: es },
|
{ code: 'fa', locale:'fa', label: 'فارسی', messages: fa },
|
||||||
{ code: 'fa', label: 'فارسی', messages: fa },
|
{ code: 'it', locale: 'it', label: 'Italiano', messages: it },
|
||||||
{ code: 'fr', label: 'Français', messages: fr },
|
{ code: 'ja', locale: 'ja', label: '日本語', messages: ja },
|
||||||
{ code: 'it', label: 'Italiano', messages: it },
|
{ code: 'ko-kr', locale: 'ko-kr', label: '한국어', messages: ko_kr },
|
||||||
{ code: 'ja', label: '日本語', messages: ja },
|
{ code: 'ne', locale: 'ne', label: 'नेपाली', messages: ne },
|
||||||
{ code: 'ko-kr', label: '한국어', messages: ko_kr },
|
{ code: 'no', locale: 'no', label: 'Norwegian', messages: no },
|
||||||
{ code: 'ne', label: 'नेपाली', messages: ne },
|
{ code: 'pl', locale: 'pl', label: 'Polski', messages: pl },
|
||||||
{ code: 'no', label: 'Norwegian', messages: no },
|
{ code: 'pt-br', locale: 'pt-br', label: 'Português - Brasil', messages: pt_br },
|
||||||
{ code: 'pl', label: 'Polski', messages: pl },
|
{ code: 'tr', locale: 'tr', label: 'Türkçe', messages: tr },
|
||||||
{ code: 'pt-br', label: 'Português - Brasil', messages: pt_br },
|
{ code: 'zh-cn', locale: 'zh-ch', label: 'Chinese - Simplified', messages: zh_cn },
|
||||||
{ code: 'ru', label: 'Russian', messages: ru },
|
{ code: 'da', locale: 'da', label: 'Danish', messages: da },
|
||||||
{ code: 'tr', label: 'Türkçe', messages: tr },
|
{ code: 'de', locale: 'de', label: 'Deutsch', messages: de},
|
||||||
{ code: 'zh-cn', label: 'Chinese - Simplified', messages: zh_cn },
|
{ code: 'ru', locale: 'ru', label: 'Russian', messages: ru },
|
||||||
]
|
]
|
||||||
|
|
||||||
/* ----------------- (do not change) ---------------- */
|
/* ----------------- (do not change) ---------------- */
|
||||||
|
@ -48,9 +48,23 @@ TRANSLATIONS.sort((a, b) => (a.code < b.code ? -1 : 1))
|
||||||
export type TDTranslation = {
|
export type TDTranslation = {
|
||||||
readonly code: string
|
readonly code: string
|
||||||
readonly label: string
|
readonly label: string
|
||||||
|
readonly locale: string
|
||||||
readonly messages: Partial<typeof en>
|
readonly messages: Partial<typeof en>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TDTranslations = TDTranslation[]
|
export type TDTranslations = TDTranslation[]
|
||||||
|
|
||||||
export type TDLanguage = TDTranslations[number]['code']
|
export type TDLanguage = TDTranslations[number]['code']
|
||||||
|
|
||||||
|
export function getTranslation(code: TDLanguage): TDTranslation {
|
||||||
|
const translation = TRANSLATIONS.find((t) => t.code === code)
|
||||||
|
|
||||||
|
const defaultTranslation = TRANSLATIONS.find((t) => t.code === 'en')!
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
...defaultTranslation.messages,
|
||||||
|
...translation?.messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { code, messages, locale: code, label: translation?.label ?? code }
|
||||||
|
}
|
Loading…
Reference in a new issue