Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions LICENSES/OFL-1.1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
SIL OPEN FONT LICENSE

Version 1.1 - 26 February 2007

PREAMBLE

The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.

The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.

DEFINITIONS

"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the copyright statement(s).

"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.

"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS

Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.

5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.

TERMINATION

This license becomes null and void if any of the above conditions are not met.

DISCLAIMER

THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ path = ["img/attach.svg", "img/description.svg", "img/details.svg", "img/toggle-
precedence = "aggregate"
SPDX-FileCopyrightText = "2018-2024 Google LLC"
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["css/fonts/Vazirmatn.woff2"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2015 The Vazirmatn Project Authors (https://github.com/rastikerdar/vazirmatn)"
SPDX-License-Identifier = "OFL-1.1"
Binary file added css/fonts/Vazirmatn.woff2
Binary file not shown.
33 changes: 32 additions & 1 deletion src/components/card/CommentForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-->

<template>
<div class="comment-form">
<div class="comment-form" :dir="contentDir">
<NcRichContenteditable v-model="commentText"
:auto-complete="autoComplete"
:maxlength="1000"
Expand Down Expand Up @@ -69,6 +69,32 @@ export default {
hasContent() {
return this.commentText.trim().length > 0
},
// Derive the form direction from the first strong character the user
// typed so the submit button (positioned with inset-inline-end) sits on
// the correct side: right for LTR text, left for Persian/Arabic. The
// editor input declares its own dir="auto", which a dir="auto" ancestor
// would skip, so we compute it here instead. Returns null while empty so
// the form simply inherits the surrounding UI direction.
contentDir() {
const text = this.commentText.replace(/<[^>]*>/g, '')
for (const ch of text) {
const code = ch.codePointAt(0)
// Latin letters (Basic Latin, Latin-1 Supplement, Latin
// Extended-A/Additional) → left-to-right.
if ((code >= 0x41 && code <= 0x5A) || (code >= 0x61 && code <= 0x7A)
|| (code >= 0xC0 && code <= 0x24F) || (code >= 0x1E00 && code <= 0x1EFF)) {
return 'ltr'
}
// Hebrew, Arabic, Arabic Supplement/Extended-A and the
// Hebrew/Arabic presentation forms → right-to-left.
if ((code >= 0x0591 && code <= 0x05F4) || (code >= 0x0600 && code <= 0x06FF)
|| (code >= 0x0750 && code <= 0x077F) || (code >= 0x08A0 && code <= 0x08FF)
|| (code >= 0xFB1D && code <= 0xFDFD) || (code >= 0xFE70 && code <= 0xFEFC)) {
return 'rtl'
}
}
return null
},
},
watch: {
value(val) {
Expand Down Expand Up @@ -125,6 +151,11 @@ export default {
z-index: 1;
}

// Point the send arrow toward the writing direction in RTL.
&[dir="rtl"] .comment-form__submit .arrow-right-icon {
transform: scaleX(-1);
}

// Add padding to prevent text from going under the button
:deep(.rich-content-editor__input) {
padding-inline-end: calc(var(--default-clickable-area) + var(--default-grid-baseline));
Expand Down
94 changes: 94 additions & 0 deletions src/css/fonts.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// Vazirmatn variable font for Persian/Arabic text. The single variable woff2
// covers every weight from 100 to 900.

// `unicode-range` restricts the font to Arabic/Persian codepoints, so the
// browser only reaches for Vazirmatn when it renders those glyphs and keeps the
// Nextcloud system font for everything else. This makes Persian/Arabic text use
// Vazirmatn regardless of the interface language (e.g. Persian content inside an
// English UI), without affecting Latin text.
@font-face {
font-family: 'Vazirmatn';
src: url('../../css/fonts/Vazirmatn.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
// ZWNJ/ZWJ (U+200C-200D, needed for Persian word joining), Arabic,
// Arabic Supplement, Arabic Extended-A, and Arabic Presentation Forms A/B.
unicode-range: U+200C-200D, U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF;
}

// Prepend Vazirmatn to the global font stack. Because of the `unicode-range`
// above the browser only uses it for Arabic/Persian glyphs; all other text
// falls through to the Nextcloud system font stack.
html,
body,
#content,
#content-vue,
input,
textarea,
button,
select {
font-family: 'Vazirmatn', var(--font-face, system-ui, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', sans-serif);
}

// The rich-text editors hard-code their own `font-family`, which overrides the
// inherited stack above. Re-assert Vazirmatn on them — it is still scoped to
// Arabic/Persian glyphs by the @font-face `unicode-range`, so Latin text keeps
// each editor's intended font. `!important` is needed to beat the components'
// scoped styles.

// Comment input — Deck's comment form is a plain `contenteditable` (vue-at);
// the mention input (NcRichContenteditable) hard-codes `font-family:
// var(--font-face)`. Re-assert Vazirmatn on both.
.comment-form__contenteditable,
.rich-contenteditable__input {
font-family: 'Vazirmatn', var(--font-face, system-ui, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', sans-serif) !important;
}

// Card description editor (EasyMDE/CodeMirror) — overrides `font-family: monospace`.
// Latin text stays monospace; only Arabic/Persian glyphs render in Vazirmatn.
.CodeMirror,
.cm-s-easymde {
font-family: 'Vazirmatn', monospace !important;
}

// Card description editor (Nextcloud Text/ProseMirror) — used instead of EasyMDE
// when the Text app is installed. Text hard-codes its own `font-family` on the
// editor content, overriding the inherited stack, so re-assert Vazirmatn here.
.description__text .ProseMirror {
font-family: 'Vazirmatn', var(--font-face, system-ui, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', 'Noto Sans', sans-serif) !important;
}

// Content-driven direction. helpers/autoTextDirection.js sets `dir="auto"` on
// editable fields (inputs, textareas, contenteditables) and on elements that
// display user content (e.g. the sidebar card title), so the browser resolves
// them to RTL when their content starts with a Persian/Arabic character.
// `text-align: start` already lands RTL text on the right, but some components
// hard-code `text-align: left`, so re-assert right alignment for the
// resolved-RTL case. `!important` beats those scoped component styles.
input:dir(rtl),
textarea:dir(rtl),
[contenteditable]:dir(rtl),
.app-sidebar-header__mainname:dir(rtl),
.app-sidebar-header__subname:dir(rtl),
.comment--content:dir(rtl) {
text-align: right !important;
}

// Right-to-left layout when the interface language is Persian or Arabic. This
// follows the UI locale rather than content; per-card direction should rely on
// the element's `dir` attribute.
html[lang^='fa'],
html[lang^='ar'] {
&,
body,
#content,
#content-vue {
direction: rtl;
}
}
73 changes: 73 additions & 0 deletions src/helpers/autoTextDirection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

// Editable fields whose direction should follow what the user types. Only
// free-text fields are included; typed inputs such as number/date/email keep
// their native direction.
const EDITABLE_SELECTOR = [
'input:not([type])',
'input[type="text"]',
'input[type="search"]',
'textarea',
'[contenteditable=""]',
'[contenteditable="true"]',
'[contenteditable="plaintext-only"]',
].join(',')

// Read-only elements that display user-generated text and should follow the
// content's direction (e.g. a Persian card title shown in an English UI).
const DISPLAY_SELECTOR = [
'.app-sidebar-header__mainname', // card/board title in the sidebar header
'.app-sidebar-header__subname', // sidebar subtitle
'.comment--content', // rendered comment body
].join(',')

const SELECTOR = `${EDITABLE_SELECTOR},${DISPLAY_SELECTOR}`

/**
* Give an element `dir="auto"` so the browser derives its base direction from
* the content: RTL for text that starts with a Persian/Arabic (first-strong)
* character, LTR otherwise. The attribute is live, so the direction keeps
* updating as the content changes, and `text-align: start` — re-asserted for
* the RTL case in css/fonts.scss — puts RTL text on the right.
*
* Elements that already declare an explicit `dir` are left untouched.
*
* @param {Element} el the candidate element
*/
function applyAutoDir(el) {
if (el instanceof HTMLElement && !el.hasAttribute('dir')) {
el.setAttribute('dir', 'auto')
}
}

/**
* Apply auto-direction to a node and any matching descendants.
*
* @param {Node} root the inserted node (or the document root for the first pass)
*/
function processTree(root) {
if (!(root instanceof HTMLElement)) {
return
}
if (root.matches(SELECTOR)) {
applyAutoDir(root)
}
root.querySelectorAll(SELECTOR).forEach(applyAutoDir)
}

// First pass over whatever is already in the DOM.
processTree(document.documentElement)

// Vue and the Nextcloud components render most fields and content lazily, so
// watch for nodes added later and tag them as they appear.
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
processTree(node)
}
}
})
observer.observe(document.documentElement, { childList: true, subtree: true })
10 changes: 10 additions & 0 deletions src/shared-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
*/
import { generateFilePath } from '@nextcloud/router'

// Vazirmatn font + RTL support for Persian/Arabic text. Loaded here because
// every entry point (main app, dashboard, reference, talk, calendar,
// collections) imports this module, so the font applies everywhere Deck runs.
import './css/fonts.scss'

// Auto-detect text direction (RTL for Persian/Arabic) in editable fields and
// in elements that display user content. Loaded here so it applies across
// every Deck entry point.
import './helpers/autoTextDirection.js'

// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken)

Expand Down