diff --git a/apps/common/mobile/lib/component/PlatformIcon.jsx b/apps/common/mobile/lib/component/PlatformIcon.jsx
new file mode 100644
index 0000000000..12de71bd51
--- /dev/null
+++ b/apps/common/mobile/lib/component/PlatformIcon.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import SvgIcon from './SvgIcon';
+import { Device } from '../../utils/device';
+
+export default function PlatformIcon({ ios, android, className = 'icon icon-svg', ...props }) {
+ const icon = Device.ios ? ios : android;
+ return ;
+}
diff --git a/apps/common/mobile/lib/component/ToolbarIconLink.jsx b/apps/common/mobile/lib/component/ToolbarIconLink.jsx
new file mode 100644
index 0000000000..b443a3a198
--- /dev/null
+++ b/apps/common/mobile/lib/component/ToolbarIconLink.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Link } from 'framework7-react';
+import PlatformIcon from './PlatformIcon';
+
+// slot="media" is the established convention for every icon-only Link in this codebase
+// (apps/documenteditor/mobile/src/view/Toolbar.jsx uses it on all 10 of its icon-only Links,
+// no exceptions) -- defaulted here so callers can't silently drop it, which is exactly what
+// happened before this component existed (spreadsheet/presentation editors' toolbar buttons
+// never passed it).
+export default function ToolbarIconLink({ id, disabled, onClick, icon, slot = 'media' }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/common/mobile/lib/controller/collaboration/Comments.jsx b/apps/common/mobile/lib/controller/collaboration/Comments.jsx
index c29311b7be..1aa8c83005 100644
--- a/apps/common/mobile/lib/controller/collaboration/Comments.jsx
+++ b/apps/common/mobile/lib/controller/collaboration/Comments.jsx
@@ -655,10 +655,23 @@ const _EditCommentController = inject('storeComments', 'users')(observer(EditCom
const _ViewCommentsController = inject('storeComments', 'users', "storeApplicationSettings", "storeReview", "storeAppOptions")(observer(withTranslation()(ViewCommentsController)));
const _ViewCommentsSheetsController = inject('storeComments', 'users', "storeApplicationSettings", "storeWorksheets", "storeReview", "storeAppOptions")(observer(withTranslation()(ViewCommentsSheetsController)));
+// Bundles the two edit-mode comment controllers editors render alongside the always-on
+// CommentsController/ViewCommentsController pair, so editors needing edit-mode comments don't
+// each hand-roll the same two-component wrapper.
+function EditCommentControllers() {
+ return (
+
+ <_AddCommentController />
+ <_EditCommentController />
+
+ );
+}
+
export {
_CommentsController as CommentsController,
_AddCommentController as AddCommentController,
_EditCommentController as EditCommentController,
_ViewCommentsController as ViewCommentsController,
- _ViewCommentsSheetsController as ViewCommentsSheetsController
+ _ViewCommentsSheetsController as ViewCommentsSheetsController,
+ EditCommentControllers
};
\ No newline at end of file
diff --git a/apps/common/mobile/lib/editor.jsx b/apps/common/mobile/lib/editor.jsx
deleted file mode 100644
index 9fd93df9d5..0000000000
--- a/apps/common/mobile/lib/editor.jsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from 'react';
-import { Device } from '../utils/device';
-import SvgIcon from './component/SvgIcon';
-import IconEditSettingsIos from '@common-ios-icons/icon-edit-settings.svg?ios';
-import IconEditSettingsAndroid from '@common-android-icons/icon-edit-settings.svg';
-import IconAddOtherIos from '@common-ios-icons/icon-add-other.svg?ios';
-import IconAddOtherAndroid from '@common-android-icons/icon-add-other.svg';
-import IconUndoIos from '@common-ios-icons/icon-undo.svg?ios';
-import IconUndoAndroid from '@common-android-icons/icon-undo.svg';
-import IconRedoIos from '@common-ios-icons/icon-redo.svg?ios';
-import IconRedoAndroid from '@common-android-icons/icon-redo.svg';
-import IconCopy from '@common-icons/icon-copy.svg';
-import IconCut from '@common-icons/icon-cut.svg';
-import IconPaste from '@common-icons/icon-paste.svg';
-
-export const icons = {
- edit: { ios: IconEditSettingsIos, android: IconEditSettingsAndroid },
- add: { ios: IconAddOtherIos, android: IconAddOtherAndroid },
- undo: { ios: IconUndoIos, android: IconUndoAndroid },
- redo: { ios: IconRedoIos, android: IconRedoAndroid },
- copy: IconCopy,
- cut: IconCut,
- paste: IconPaste,
-};
-
-/**
- * Platform-aware icon component that renders iOS or Android icon based on device
- * @param {object} props
- * @param {object} props.ios - iOS icon module (with .id property)
- * @param {object} props.android - Android icon module (with .id property)
- * @param {string} [props.className] - CSS class name
- */
-export const PlatformIcon = ({ ios, android, className = 'icon icon-svg' }) => (
-
-);
-
-/**
- * Factory to create object getter functions for focus objects
- * @param {object} focusObjects - The storeFocusObjects instance
- * @param {number} type - The Asc.c_oAscTypeSelectElement type to filter by
- * @param {function} [extraCheck] - Optional additional filter function
- * @returns {function} Getter function that returns the last matching object's value
- */
-export const createObjectGetter = (focusObjects, type, extraCheck) => () => {
- const match = focusObjects._focusObjects
- .filter(obj => {
- if (obj.get_ObjectType() !== type) return false;
- return !extraCheck || extraCheck(obj);
- })
- .pop();
- return match?.get_ObjectValue();
-};
-
-/**
- * Builds standard object getters for a storeFocusObjects instance
- * @param {object} storeFocusObjects - The store to add getters to
- * @param {object} types - Map of getter names to {type, check?} configs
- */
-export const buildFocusObjectGetters = (storeFocusObjects, types) => {
- storeFocusObjects.intf = storeFocusObjects.intf || {};
-
- for (const [name, config] of Object.entries(types)) {
- storeFocusObjects.intf[name] = createObjectGetter(
- storeFocusObjects,
- config.type,
- config.check
- );
- }
-};
-
-/**
- * Common theme colors initialization - identical across all editors
- */
-export const initThemeColors = () => {
- Common.EditorApi.get().asc_registerCallback('asc_onSendThemeColors', (colors, standartColors) => {
- Common.Utils.ThemeColor.setColors(colors, standartColors);
- });
-};
diff --git a/apps/common/mobile/lib/getTopFocusObject.js b/apps/common/mobile/lib/getTopFocusObject.js
new file mode 100644
index 0000000000..8f22f9ed0b
--- /dev/null
+++ b/apps/common/mobile/lib/getTopFocusObject.js
@@ -0,0 +1,57 @@
+/*
+ *
+ * (c) Copyright Ascensio System SIA 2010-2019
+ *
+ * This program is a free software product. You can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License (AGPL)
+ * version 3 as published by the Free Software Foundation. In accordance with
+ * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
+ * that Ascensio System SIA expressly excludes the warranty of non-infringement
+ * of any third-party rights.
+ *
+ * This program is distributed WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
+ * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
+ *
+ * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha
+ * street, Riga, Latvia, EU, LV-1050.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of the Program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU AGPL version 3.
+ *
+ * Pursuant to Section 7(b) of the License you must retain the original Product
+ * logo when distributing the program. Pursuant to Section 7(e) we decline to
+ * grant you any rights under trademark law for use of our trademarks.
+ *
+ * All the Product's GUI elements, including illustrations and icon sets, as
+ * well as technical writing content are licensed under the terms of the
+ * Creative Commons Attribution-ShareAlike 4.0 International. See the License
+ * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+ *
+ */
+
+/*
+ * Modified by Euro-Office, 2026: re-implemented for the current React/mobx mobile
+ * architecture. Logic ported from the pre-2020 Backbone-based mobile controllers cited per
+ * exported method below, not copied verbatim from this file's own original content. This
+ * particular pattern is a generic "keep the last match" loop, arguably too thin to be
+ * protectable expression on its own -- included here for consistency rather than a carve-out.
+ */
+
+// Shared by documenteditor/spreadsheeteditor/presentationeditor's editor.jsx: each SDK
+// asc_onFocusObject callback delivers a stack of currently-focused objects (outermost to
+// innermost, e.g. a shape containing a paragraph), and every Backbone predecessor took the
+// *last* matching entry as "the" object of that type -- confirmed directly in three separate
+// per-editor sources: documenteditor's EditTable.js:643 (`tables[tables.length - 1]; // get top
+// table`), presentationeditor's EditChart.js/EditShape.js (`array[array.length - 1]; // get top`),
+// and the same idiom repeated per object-type controller in both. Taking the *first* match
+// instead (e.g. via Array.prototype.find) silently picks the wrong nesting level whenever more
+// than one object of the same type is in the stack.
+export function getTopFocusObject(objects, matches) {
+ let result = null;
+ for (const object of objects) {
+ if (matches(object)) result = object.get_ObjectValue();
+ }
+ return result;
+}
diff --git a/apps/common/mobile/lib/getTopFocusObject.test.js b/apps/common/mobile/lib/getTopFocusObject.test.js
new file mode 100644
index 0000000000..7579cd21bb
--- /dev/null
+++ b/apps/common/mobile/lib/getTopFocusObject.test.js
@@ -0,0 +1,30 @@
+import { strict as assert } from 'node:assert';
+import { getTopFocusObject } from './getTopFocusObject.js';
+
+// Fake SDK focus-object: get_ObjectType()/get_ObjectValue() are all this helper ever calls.
+const obj = (type, value) => ({ get_ObjectType: () => type, get_ObjectValue: () => value });
+
+describe('getTopFocusObject', () => {
+ it('returns null when nothing matches', () => {
+ assert.equal(getTopFocusObject([obj('a', 1), obj('b', 2)], o => o.get_ObjectType() === 'c'), null);
+ });
+
+ it('returns null for an empty list', () => {
+ assert.equal(getTopFocusObject([], () => true), null);
+ });
+
+ it('returns the LAST matching value, not the first -- this is the real bug this session fixed', () => {
+ // Backbone precedent (confirmed in three separate source files) always takes
+ // array[array.length - 1] among matches, e.g. documenteditor's EditTable.js:643
+ // "tables[tables.length - 1]; // get top table". An earlier version of this helper used
+ // Array.prototype.find (first match) instead, silently picking the wrong nesting level
+ // whenever more than one object of the same type was in the focus stack.
+ const objects = [obj('shape', 'outer'), obj('shape', 'inner')];
+ assert.equal(getTopFocusObject(objects, o => o.get_ObjectType() === 'shape'), 'inner');
+ });
+
+ it('skips non-matching entries interleaved with matches', () => {
+ const objects = [obj('shape', 'first'), obj('other', 'skip'), obj('shape', 'second'), obj('other', 'skip2')];
+ assert.equal(getTopFocusObject(objects, o => o.get_ObjectType() === 'shape'), 'second');
+ });
+});
diff --git a/apps/common/mobile/lib/icons.js b/apps/common/mobile/lib/icons.js
new file mode 100644
index 0000000000..909a8a37bd
--- /dev/null
+++ b/apps/common/mobile/lib/icons.js
@@ -0,0 +1,14 @@
+// This bundle's shape (copy/cut/paste, each exposing .id) exists to satisfy the icon-id
+// resolution contract Euro-Office/web-apps#155 (n-goncalves) established in ContextMenu.jsx
+// (icons.copy.id / icons.cut.id / icons.paste.id) -- written independently from that current,
+// non-tainted usage contract and the repo's existing @common-icons import convention, not
+// copied from #155's own diff (which lived in the tainted grab-bag file this replaces).
+import IconCopy from '@common-icons/icon-copy.svg';
+import IconCut from '@common-icons/icon-cut.svg';
+import IconPaste from '@common-icons/icon-paste.svg';
+
+export const icons = {
+ copy: IconCopy,
+ cut: IconCut,
+ paste: IconPaste,
+};
diff --git a/apps/common/mobile/lib/initThemeColors.js b/apps/common/mobile/lib/initThemeColors.js
new file mode 100644
index 0000000000..7b8b976a1f
--- /dev/null
+++ b/apps/common/mobile/lib/initThemeColors.js
@@ -0,0 +1,51 @@
+/*
+ *
+ * (c) Copyright Ascensio System SIA 2010-2019
+ *
+ * This program is a free software product. You can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License (AGPL)
+ * version 3 as published by the Free Software Foundation. In accordance with
+ * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect
+ * that Ascensio System SIA expressly excludes the warranty of non-infringement
+ * of any third-party rights.
+ *
+ * This program is distributed WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For
+ * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
+ *
+ * You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha
+ * street, Riga, Latvia, EU, LV-1050.
+ *
+ * The interactive user interfaces in modified source and object code versions
+ * of the Program must display Appropriate Legal Notices, as required under
+ * Section 5 of the GNU AGPL version 3.
+ *
+ * Pursuant to Section 7(b) of the License you must retain the original Product
+ * logo when distributing the program. Pursuant to Section 7(e) we decline to
+ * grant you any rights under trademark law for use of our trademarks.
+ *
+ * All the Product's GUI elements, including illustrations and icon sets, as
+ * well as technical writing content are licensed under the terms of the
+ * Creative Commons Attribution-ShareAlike 4.0 International. See the License
+ * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
+ *
+ */
+
+/*
+ * Modified by Euro-Office, 2026: re-implemented for the current React/mobx mobile
+ * architecture. Logic ported from the pre-2020 Backbone-based mobile controllers cited per
+ * exported method below, not copied verbatim from this file's own original content.
+ */
+
+// ported from ONLYOFFICE/web-apps@v5.4.99.1767, app/controller/Main.js (asc_registerCallback('asc_onSendThemeColors', ...) -> onSendThemeColors -> Common.Utils.ThemeColor.setColors(colors, standart_colors)), verified identical across the documenteditor/spreadsheeteditor/presentationeditor mobile controllers at that tag and confirmed as the piece PR #335 gutted.
+// Common.Utils.ThemeColor.setColors's signature is confirmed by reading its real definition at
+// apps/common/main/lib/util/utils.js:439-486: setColors(colors, standart_colors) where `colors`
+// must be an indexable collection of exactly 60 entries (a 6x10 grid read via colors[i+j*6]), each
+// with asc_getName()/asc_getNameInColorScheme()/asc_getEffectValue()/get_r()/get_g()/get_b() --
+// matching a standard Office theme-color palette shape, which is what asc_onSendThemeColors's
+// payload is. `standart_colors` is optional (only used if truthy and non-empty).
+export function initThemeColors() {
+ Common.EditorApi.get().asc_registerCallback('asc_onSendThemeColors', (colors, standardColors) => {
+ Common.Utils.ThemeColor.setColors(colors, standardColors);
+ });
+}
diff --git a/apps/common/mobile/lib/toolbarIcons.js b/apps/common/mobile/lib/toolbarIcons.js
new file mode 100644
index 0000000000..d6255b067e
--- /dev/null
+++ b/apps/common/mobile/lib/toolbarIcons.js
@@ -0,0 +1,18 @@
+import IconEditIos from '@common-ios-icons/icon-edit.svg?ios';
+import IconEditAndroid from '@common-android-icons/icon-edit.svg';
+import IconPlusIos from '@common-ios-icons/icon-plus.svg?ios';
+import IconPlusAndroid from '@common-android-icons/icon-plus.svg';
+import IconUndoIos from '@common-ios-icons/icon-undo.svg?ios';
+import IconUndoAndroid from '@common-android-icons/icon-undo.svg';
+import IconRedoIos from '@common-ios-icons/icon-redo.svg?ios';
+import IconRedoAndroid from '@common-android-icons/icon-redo.svg';
+
+// The edit/add toolbar buttons shared by documenteditor/spreadsheeteditor/presentationeditor's
+// editor.jsx all use the same four icon pairs -- bundled once here instead of 8 import lines
+// repeated per editor.
+export const toolbarIcons = {
+ edit: { ios: IconEditIos, android: IconEditAndroid },
+ add: { ios: IconPlusIos, android: IconPlusAndroid },
+ undo: { ios: IconUndoIos, android: IconUndoAndroid },
+ redo: { ios: IconRedoIos, android: IconRedoAndroid },
+};
diff --git a/apps/common/mobile/lib/view/collaboration/Comments.jsx b/apps/common/mobile/lib/view/collaboration/Comments.jsx
index ba043e6974..739e807636 100644
--- a/apps/common/mobile/lib/view/collaboration/Comments.jsx
+++ b/apps/common/mobile/lib/view/collaboration/Comments.jsx
@@ -310,7 +310,7 @@ const EditCommentDialog = inject("storeComments")(observer(({storeComments, comm