| FoxitPDFSDKforWeb v11.0.5 Archives Foxit PDF SDK for Web

/builds/foxit/phantom/cloud/phantompdf_online/foxitwebsdk/doxygen/workspace/headerfiles/uix-addons/thumbnail/thumbUIComponent.js

// from `1` to `pageCount` { pages: [1,2,3,5,6,8], pagesGroup: [[1,2,3], [5,6], [8]], pageRangeGroup: [[1,3], [5,6], [8]], pageRangeStrings: ["1-3", "5-6", "8"], pageRangeString: "1-3, 5-6, 8", isSelected: true }

import CompilePannelTemplate from "./templates/thumb.art";
import compileThumbTemplate from "./templates/thumb-item.art";
import * as UIExtension from "UIExtension";
import _debounce from "lodash/debounce";
const __ = UIExtension.__;
const $ = UIExtension.vendors.jQuery;
const COMPONENT_EVENTS = UIExtension.UIConsts.COMPONENT_EVENTS;
const PDFDocPermission = UIExtension.PDFViewCtrl.Consts.PDFDocPermission;
const Events = UIExtension.PDFViewCtrl.ViewerEvents;
const UIEvents = UIExtension.UIEvents;
import "./style/thumb.scss";
import Dragable from '../../src/shared/Dragable.js';
import { htmlCharacter2Entity } from "../../src/shared/html-util";
import DragFileInsert from './dragFileInsert';
import {
THUMBNAIL_PAGE_BASIS_WIDTH,
THUMBNAIL_PAGE_BASIS_HEIGHT,
adjustContainerFlexBox,
resetContainerStyle,
resetScale,
showScaleContextmenu
} from "./contextmenu/thumbScale";
import { isPageLabelSetting } from "../../src/shared/feature-common/formatPageLabel";
import { ThumbnailZoomService } from './zoom/ThumbnailZoomService';
import { DebounceAsyncTaskExecutor } from './core';
const THUMB_BORDER_WIDTH = 1;
const RECT_BORDER_WIDTH = 2;
const {
PDFViewCtrl: {
BrowserInfo: { isSafari },
DeviceInfo: {
isTouchDevice, isDesktop, isMacOS
},
shared: {
attachContextMenuEvent
}
}
} = UIExtension;
export default class ThumbUIComponent extends UIExtension.components
.ContainerComponent {
static getName() {
return "thumbnail-panel";
}
static inject() {
return {
zoomService: ThumbnailZoomService,
};
}
constructor(...props) {
super(...props);
// Control the frequency of thumbnail updates.
this.updateVisiblePages = _debounce(this.updateVisiblePages.bind(this), 100);
this.evTnRectDown = null;
this.evDocMove = null;
this.evDocUp = null;
this.moving = false; // 红框是否处于移动状态
this.resizing = false; // 红框是否处于调整状态
this.cachePageIndex = -1;
this.prevPos = {x: 0, y: 0};
}
allInjected() {
const executor = new DebounceAsyncTaskExecutor()
this.addDestroyHook(
this.zoomService.onScaleChanged((scaleRatio, isMin, isMax, isSmall) => {
executor.exec(() => {
return this.getPDFUI().getPDFViewer().then(pdfViewer => {
if(!pdfViewer.getCurrentPDFDoc()) {
return;
}
return this._updateOnScaleChanged(scaleRatio, isSmall);
});
});
}),
() => executor.cancel()
)
}
_updateOnScaleChanged(scaleRatio, isSmall) {
const { $thumbImageContainer, $thumbImageContainerBox} = this;
const thumbImageContainerBox = $thumbImageContainerBox.get(0);
const thumbImageContainer = $thumbImageContainer.get(0);
thumbImageContainer.classList.toggle('small-size', isSmall);
const pageWidth = THUMBNAIL_PAGE_BASIS_WIDTH * scaleRatio;
const pageHeight = THUMBNAIL_PAGE_BASIS_HEIGHT * scaleRatio;
const pages = thumbImageContainer.querySelectorAll('.fv__thumb-page');
pages.forEach(el => {
el.style.cssText += `
width: ${pageWidth}px;
height: ${pageHeight}px
`;
});
const containerBoxWidth = thumbImageContainerBox.getBoundingClientRect().width;
const containerWidth = thumbImageContainer.getBoundingClientRect().width;
// reduce triggered, reduce the container's width if container more than outer box's width
if (pageWidth < containerWidth && containerWidth > containerBoxWidth) {
$thumbImageContainer.css({ width: Math.max(pageWidth, containerBoxWidth) });
} else if (containerWidth < pageWidth) {
// enlarge triggered, enlarge the container's width if container less than page's width
$thumbImageContainer.css({ width: pageWidth });
} else {
$thumbImageContainer.css({ width: 'auto' });
}
return this.updateVisiblePages();
}
createDOMElement() {
return document.createElement("div");
}
getContainerElement() {
return this.$html.find(".fv__thumb-image-container")[0];
}
render() {
super.render();
this.element.classList.add('fv__ui-thumbnail-panel-body');
let $element = $(this.element);
$element[0].innerHTML = CompilePannelTemplate();
this.$thumbImageContainerBox = $element.find(
".fv__thumb-image-container-box"
);
this.$thumbImageContainer = $element.find(".fv__thumb-image-container");
this.$html = $element;
}
mounted() {
super.mounted();
// 右键菜单开关
const isCollaborationMode = this.pdfUI.customs && this.pdfUI.customs.isCollaborationMode();
this.contextmenuSwitch = isCollaborationMode ? 'off' : 'on';
this.$thumbImageContainerBox.scroll(e => {
this.updateVisiblePages();
});
this.addDragEvent();
if (
navigator &&
navigator.userAgent &&
navigator.userAgent.indexOf("Firefox") > -1
) {
this.element.addEventListener("drop", e => {
e.preventDefault();
e.stopPropagation();
});
}
const contextmenu = this.getRoot().getComponentByName("fv--thumbnail-contextmenu");
if (contextmenu) {
const {
ModifyDocument: modifyPermission, Assemble, Extract: extractPermission
} = UIExtension.PDFViewCtrl.Consts.PDFDocPermission;
const assemblePermission = modifyPermission + Assemble;
const contextMenuItemCopy = contextmenu.getComponentByName("contextmenu-item-thumbnail-copy");
const contextMenuItemPaste = contextmenu.getComponentByName("contextmenu-item-thumbnail-paste");
const contextMenuItemDelete = contextmenu.getComponentByName("contextmenu-item-thumbnail-delete");
const contextMenuPageReverse = contextmenu.getComponentByName("organize-tab-page-reverse");
const contextMenuItemExtract = contextmenu.getComponentByName("contextmenu-item-thumbnail-extract");
const contextMenuItemSplit = contextmenu.getComponentByName("contextmenu-item-thumbnail-split");
const contextMenuItemRotate = contextmenu.getComponentByName("contextmenu-item-thumbnail-rotate");
const contextMenuItemRotateLeft = contextmenu.getComponentByName("contextmenu-item-thumbnail-rotate-left");
const contextMenuItemRotateRight = contextmenu.getComponentByName("contextmenu-item-thumbnail-rotate-right");
const contextmenuItemThumbnailMove = contextmenu.getComponentByName('contextmenu-item-thumbnail-move');
const contextMenuItemEnlarge = contextmenu.getComponentByName('contextmenu-item-thumbnail-enlarge');
const contextMenuItemReduce = contextmenu.getComponentByName('contextmenu-item-thumbnail-reduce');
// 获取swap pages 右键菜单
const contextmenuItemThumbnailSwap = contextmenu.getComponentByName('contextmenu-item-thumbnail-swap');
// 获取replace pages 右键菜单
const contextmenuItemThumbnailReplace = contextmenu.getComponentByName('contextmenu-item-thumbnail-replace');
const contextmenuItemThumbnailCrop = contextmenu.getComponentByName('contextmenu-item-thumbnail-crop');
const contextmenuItemThumbnailDuplicate = contextmenu.getComponentByName('contextmenu-item-thumbnail-duplicate');
const contextmenuItemEmbed = contextmenu.getComponentByName('contextmenu-item-thumbnail-embed');
const contextmenuItemRemoveEmbedded = contextmenu.getComponentByName('contextmenu-item-thumbnail-remove-embedded');
const contextmenuItemFormatNumbering = contextmenu.getComponentByName('contextmenu-item-thumbnail-format-numbering');
const contextmenuItemSepReversePage = contextmenu.getComponentByName('contextmenu-item-sep-reverse-page');
const contextmenuItemAddBlankPage = contextmenu.getComponentByName('contextmenu-item-thumbnail-add-blank-page');
const contextmenuItemPageTransitions = contextmenu.getComponentByName('contextmenu-item-thumbnail-pageTransitions');
const pageProperties = contextmenu.getComponentByName('contextmenu-item-thumbnail-properties');
const contextmenuItemInsertPage = document.querySelector('#contextmenu-item-insert-page');
const contextmenuOffHandle = contextmenu.on(COMPONENT_EVENTS.SHOWN, () => {
if (this.pdfUI.pdfViewer.currentPDFDoc.viewAutoScrolling) {
contextmenu.disable();
return;
}
contextmenu.enable();
this.getPDFUI()
.getPDFDocRender()
.then(docRender => {
const permission = docRender.getUserPermission().getValue();
const isXFA = docRender.doc.isXFA();
if (permission & modifyPermission) {
contextMenuItemCopy && contextMenuItemCopy.enable();
contextMenuItemPaste && contextMenuItemPaste.enable();
contextMenuPageReverse && contextMenuPageReverse.enable();
contextmenuItemThumbnailMove && contextmenuItemThumbnailMove.enable();
contextmenuItemThumbnailSwap && contextmenuItemThumbnailSwap.enable();
contextmenuItemThumbnailCrop && contextmenuItemThumbnailCrop.enable();
contextmenuItemThumbnailDuplicate && contextmenuItemThumbnailDuplicate.enable();
contextmenuItemThumbnailReplace && contextmenuItemThumbnailReplace.enable();
contextmenuItemEmbed && contextmenuItemEmbed.enable();
contextmenuItemRemoveEmbedded && contextmenuItemRemoveEmbedded.enable();
contextmenuItemFormatNumbering && contextmenuItemFormatNumbering.enable();
contextmenuItemPageTransitions && contextmenuItemPageTransitions.enable();
pageProperties && pageProperties.enable();
// if clipboard not initialized, disabled paste
if (this.controller.recordPagesToClipboard) {
contextMenuItemPaste && contextMenuItemPaste.enable();
} else {
contextMenuItemPaste && contextMenuItemPaste.disable();
}
} else {
contextMenuItemCopy && contextMenuItemCopy.disable();
contextMenuItemPaste && contextMenuItemPaste.disable();
contextMenuPageReverse && contextMenuPageReverse.disable();
contextmenuItemThumbnailMove && contextmenuItemThumbnailMove.disable();
contextmenuItemThumbnailSwap && contextmenuItemThumbnailSwap.disable();
contextmenuItemThumbnailCrop && contextmenuItemThumbnailCrop.disable();
contextmenuItemThumbnailDuplicate && contextmenuItemThumbnailDuplicate.disable();
contextmenuItemThumbnailReplace && contextmenuItemThumbnailReplace.disable();
contextmenuItemEmbed && contextmenuItemEmbed.disable();
contextmenuItemRemoveEmbedded && contextmenuItemRemoveEmbedded.disable();
contextmenuItemFormatNumbering && contextmenuItemFormatNumbering.disable();
contextmenuItemPageTransitions && contextmenuItemPageTransitions.disable();
pageProperties && pageProperties.disable();
}
// 编辑权限
if (permission & assemblePermission) {
contextMenuItemDelete && contextMenuItemDelete.enable();
contextMenuItemRotate && contextMenuItemRotate.enable();
contextMenuItemRotateLeft && contextMenuItemRotateLeft.enable();
contextMenuItemRotateRight && contextMenuItemRotateRight.enable();
contextmenuItemAddBlankPage && contextmenuItemAddBlankPage.enable();
} else {
contextMenuItemDelete && contextMenuItemDelete.disable();
contextMenuItemRotate && contextMenuItemRotate.disable();
contextMenuItemRotateLeft && contextMenuItemRotateLeft.disable();
contextMenuItemRotateRight && contextMenuItemRotateRight.disable();
contextmenuItemAddBlankPage && contextmenuItemAddBlankPage.disable();
}
if (this.$pages.length <= 1) {
contextMenuItemDelete && contextMenuItemDelete.disable();
contextmenuItemThumbnailMove && contextmenuItemThumbnailMove.disable();
contextmenuItemThumbnailSwap && contextmenuItemThumbnailSwap.disable();
}
// split extra 权限判断
if (permission & extractPermission) {
contextMenuItemExtract && contextMenuItemExtract.enable();
contextMenuItemSplit && contextMenuItemSplit.enable();
} else {
contextMenuItemExtract && contextMenuItemExtract.disable();
contextMenuItemSplit && contextMenuItemSplit.disable();
}
if (!this.checkAddonInstance('MovePagesUIXAddon') || this.controller.isStaticXFA) {
contextmenuItemThumbnailMove && contextmenuItemThumbnailMove.disable();
}
const isProtectedView = localStorage.getItem('SECURITY_PROTECTED_VIEW') === 'all' ? true:false;
const arr = [contextMenuItemCopy,contextMenuItemPaste,contextMenuItemExtract,contextMenuItemSplit,contextMenuItemRotate,contextMenuItemRotateLeft,contextMenuItemRotateRight,contextmenuItemThumbnailMove,contextmenuItemThumbnailSwap,contextmenuItemThumbnailReplace,contextmenuItemThumbnailCrop,contextmenuItemThumbnailDuplicate,contextmenuItemEmbed,contextmenuItemRemoveEmbedded,contextmenuItemFormatNumbering,contextMenuPageReverse, contextmenuItemAddBlankPage, contextmenuItemPageTransitions];
if (isProtectedView || this.$pages.length > 1) {
arr.push(contextMenuItemDelete);
}
if(isProtectedView){
arr.forEach(m=> m && m.hide());
contextmenuItemInsertPage && (contextmenuItemInsertPage.style.display='none');
setTimeout(()=>{
contextMenuPageReverse && contextMenuPageReverse.hide();
contextmenuItemSepReversePage && contextmenuItemSepReversePage.hide();
})
}else{
arr.forEach(m=>m && m.show());
contextmenuItemInsertPage && (contextmenuItemInsertPage.style.display='block');
}
if (this.showContextMenuPosition) {
contextmenu.updatePosition(this.showContextMenuPosition.x, this.showContextMenuPosition.y);
this.showContextMenuPosition = null;
}
return isXFA;
}).then(res => {
if(res) {
contextMenuItemRotate && contextMenuItemRotate.disable();
contextMenuItemRotateLeft && contextMenuItemRotateLeft.disable();
contextMenuItemRotateRight && contextMenuItemRotateRight.disable();
contextMenuItemDelete && contextMenuItemDelete.disable();
contextmenuItemAddBlankPage && contextmenuItemAddBlankPage.disable()
contextmenuItemThumbnailMove && contextmenuItemThumbnailMove.disable();
contextmenuItemThumbnailSwap && contextmenuItemThumbnailSwap.disable();
contextMenuItemSplit && contextMenuItemSplit.disable();
contextmenuItemThumbnailCrop && contextmenuItemThumbnailCrop.disable();
contextmenuItemThumbnailDuplicate && contextmenuItemThumbnailDuplicate.disable();
contextmenuItemInsertPage && contextmenuItemInsertPage.classList.add("disabled");
} else {
contextmenuItemInsertPage && contextmenuItemInsertPage.classList.remove("disabled");
}
})
});
this.addDestroyHook(contextmenuOffHandle);
this.addDestroyHook(() => {
this.getPDFUI().addViewerEventListener('permission-changed', pdfDoc => contextmenuOffHandle);
});
if (WEBPACK_ONLINE_ONLY && !this.hasBindSubMenuHoverEvent) {
this.hasBindSubMenuHoverEvent = true;
const insertFromFile = contextmenu.getComponentByName("organize-tab-page-insert-from-file");
this.bindSubMenuHoverEvent(insertFromFile.parent, this);
this.bindSubMenuHoverEvent(contextmenuItemThumbnailMove.parent, this);
}
}
const detachContextmenu = attachContextMenuEvent(
this.element,
e => {
const ePageThumb = e.target.closest("[data-page-index]");
if (!ePageThumb) {
// trigger contextmenu event, not on the page, show the scale contextmenu
if (showScaleContextmenu(this.getRoot(), e)) {
e.preventDefault();
}
return;
}
const pageIndex = +ePageThumb.getAttribute("data-page-index");
const contextmenu = this.getRoot().getComponentByName(
"fv--thumbnail-contextmenu"
);
if(this.contextmenuSwitch === 'off' || !contextmenu){return}
this.showContextMenuPosition = { x: e.clientX, y: e.clientY };
contextmenu.setCurrentTarget(pageIndex);
contextmenu.showAt(e.clientX, e.clientY);
e.preventDefault();
},
true, // addEventListener 方法参数
(!isDesktop && isTouchDevice) // 使用兼容方案（sortable阻止了contextmenu事件触发）
);
this.addDestroyHook(detachContextmenu);
// 监听鼠标左右键和ctrl组合
this.$thumbImageContainerBox.on("mouseup", ".fv__thumb-page", e => {
if (e.button !== 0 && e.button !== 2) { // 只处理鼠标左右键
return;
}
const cnt = this.$thumbImageContainer.find(".fv__thumb-page").length;
if(cnt < 1) {
return;
}
let $pageThumb = $(e.currentTarget);
const pageIndex = $pageThumb.data("page-index");
const isCtrlKey = (isMacOS && e.metaKey) || (!isMacOS && e.ctrlKey);
if (isCtrlKey) {
// 多选
if (e.button === 2) {
$pageThumb.addClass("fv__active");
} else {
$pageThumb.toggleClass("fv__active");
}
} else {
// 单选
const isCurrentItemActive = this.$thumbImageContainer.find('.fv__thumb-page.fv__active[data-page-index="' + pageIndex + '"]').length > 0;
if (!(e.button === 2 && isCurrentItemActive)) {
this.$html.find(".fv__active").removeClass("fv__active");
}
$pageThumb.addClass("fv__active");
}
this.fromClickThumb = pageIndex;
this.doPageAction(this.previePageIndex, pageIndex);
this.previePageIndex = pageIndex;
this.getPDFUI().goToPage(pageIndex, 0, 0, false);
(!this.resizing && !this.moving) && this.calcThumbnailRect(true, pageIndex);
});
this.addDestroyHook(() => {
this.$thumbImageContainerBox.off("mouseup");
});
// 监听删除键
const delKeyHandle = async (e) => {
const defaultDeleteBinderName = this.getPDFUI().pdfViewer.keyboard.defaultDeleteBinderName;
if(!this.parent.isActive || defaultDeleteBinderName !== 'thumbUIComponent') {
return;
}
if (this.getPDFUI().pdfViewer.getAllActivatedElements().length) return;
const docRender = await this.getPDFUI().getPDFDocRender();
if(!docRender) {
return;
}
const permission = docRender.getUserPermission().getValue();
if (
!(
permission & PDFDocPermission.Assemble ||
permission & PDFDocPermission.ModifyDocument
)
) {
return;
}
// fix 在有弹框和其他focus的情况不能触发删除
const targetClassName = e.nativeEvent.target.className;
const modal = $('.fv__ui-layer-modal');
if((modal.length > 0 || targetClassName !== 'fv__thumb-image-container-box')) return;
const $pages = this.$pages;
const $activePages = this.$html.find(".fv__active");
if ($pages.length <= 1) {
this.getPDFUI().alert("thumbnail:removePageAlert", undefined, undefined, 'warning');
return;
}
if ($activePages.length >= $pages.length) {
this.getPDFUI().alert("thumbnail:removePageAlert", undefined, undefined, 'warning');
return;
}
this.getPDFUI().confirm("thumbnail:removePageConfirm", undefined, undefined, 'prompt').then(async () => {
let ids = [];
$activePages.each((index, item) => {
ids.push($(item).data("page-index"));
});
if (ids.length > 0) {
ids = ids.sort(function(x, y) {
return y - x;
});
for (let i=0; i<ids.length; i++) {
await this.controller.deletePageByIndex(ids[i]);
await new Promise(resolve => setTimeout(resolve, 50));
}
// Ensure indexesRange from small to large.
const indexesRange = Array.from(ids).reverse().map((index) => [index, index]);
this.controller.currentPDFDoc.updatePageLabelsByAction('deletePages', [indexesRange]);
}
}, () => {});
};
this.getPDFUI().pdfViewer.onShortcutKey('delete', delKeyHandle);
this.addDestroyHook(() => {
this.getPDFUI().pdfViewer.offShortcutKey('delete', delKeyHandle);
});
}
bindSubMenuHoverEvent(component, menuParentComponent) {
const element = component?.element;
if (!element) return;
const mouseEnterHandler = () => {
const isDisabled = component.disabled || element.classList.contains('disabled');
if(!isDisabled) {
component.active();
}
}
const mouseLeaveHandler = () => {
component.deactive();
}
const isDropDown = component.nodeName === 'dropdown' || element.classList.contains('fv__ui-dropdown');
if (isDropDown) {
// 如果是下拉菜单, 重置toggle方法，防止下拉菜单被点击时，toggle方法被调用 隐藏下拉菜单
component.toggle = () => {}
}
// 绑定鼠标进入和离开事件
element.addEventListener('mouseenter', mouseEnterHandler);
element.addEventListener('mouseleave', mouseLeaveHandler);
// 绑定销毁事件
menuParentComponent.addDestroyHook(() => {
element.removeEventListener('mouseenter', mouseEnterHandler);
element.removeEventListener('mouseleave', mouseLeaveHandler);
});
}
dragFileInsert(data) {
// 自定义发送GA事件
this.pdfUI.customs.sendGAEvent({
category: 'Organize',
label: 'organize_tab',
action: 'insert_page_by_dragging',
eventKey: 'Insert page by dragging'
});
const isAccountExpired = this.pdfUI.customs.isAccountExpired();
if (isAccountExpired) {
this.dragable._removeClass(this.dragable.options.chosenClass, data.target);
return;
}
const dragFileInsert = DragFileInsert.getInstance({
component: this,
pdfUI: this.pdfUI
});
dragFileInsert.start(data);
}
// 是否可拖动
enableDragAndDrop(enable) {
if (!this.dragable) return;
this.dragable.options.allowDrag = !!enable;
// this.sortable.options.disabled = !enable;
// this.sortable.options.disabled = true;
}
async _checkAndGetLabels() {
// 检查 doc 是否标记了 page label
// 如果标记，获取 labels
const hasLabel = await this.controller.currentPDFDoc.checkDocHasPageLabel();
let labels = null;
if (hasLabel) {
labels = await this.controller.currentPDFDoc.getPageLabels();
}
return { hasLabel, labels };
}
// 初始化项
async initPageItems(pageCount) {
const logicalPageNumChecked = isPageLabelSetting();
let hasLabel = false;
let labels = [];
if (logicalPageNumChecked) {
const labelsObj = await this._checkAndGetLabels();
hasLabel = labelsObj.hasLabel;
labels = labelsObj.labels;
}
let $oldPages = this.$pages;
if (!$oldPages || $oldPages.length == 0) {
let pageItems = "";
for (let i = 0; i < pageCount; i++) {
const label = logicalPageNumChecked && hasLabel ? htmlCharacter2Entity(labels[i]) : i + 1;
pageItems += compileThumbTemplate({ index: i, label });
}
this.$thumbImageContainer.append(pageItems);
} else {
this.$pages.find('.fv__thumb-page-container').addClass("loading");
let $canvases = this.$thumbImageContainer.find("canvas");
$canvases.each(index => $canvases[index].width = $canvases[index].width);
this.$thumbImageContainer.find(".fv__thumb-page-index").each((i, el) => {
if (i >= pageCount) {
return;
}
const label = logicalPageNumChecked && hasLabel ? htmlCharacter2Entity(labels[i]) : i + 1;
$(el).html(label);
});
let diffCount = pageCount - $canvases.length;
let index = 0;
if (diffCount < 0) {
while (index < -diffCount) {
$($oldPages.get($canvases.length - index - 1)).remove();
index++;
}
} else {
let pageItems = "";
for (let i = 0; i < diffCount; i++) {
const idx = $canvases.length + i;
const label = hasLabel ? htmlCharacter2Entity(labels[idx]) : idx + 1;
pageItems += compileThumbTemplate({ index: idx, label });
}
this.$thumbImageContainer.append(pageItems);
}
$oldPages.show();
}
this.$pages = this.$thumbImageContainer.find(".fv__thumb-page");
this.visiblePages = [];
this.initThumbnailRect();
}
// 初始化缩略图显示框
initThumbnailRect() {
if (!isDesktop) return;
let rectTarget = null;
let startPos = { x: 0, y: 0, w: 0, h: 0 };
let elePos = { x: 0, y: 0 };
let eleRect = {};
let parentRect = {};
let thumbRect = {};
let $rects = this.$thumbImageContainer.find('.fv__thumb-view-rect');
!this.evTnRectDown && $rects.on('mousedown', this.evTnRectDown = ev => {
if (ev.target.className.indexOf('fv__thumb-view-rect') >= 0) {
ev.stopPropagation();
let target = ev.target;
eleRect = target.getBoundingClientRect();
parentRect = target.parentElement.getBoundingClientRect();
thumbRect = $(target.parentElement).find('.fv__thumb-image').get(0).getBoundingClientRect();
let mouseX = ev.clientX - eleRect.left;
let mouseY = ev.clientY - eleRect.top;
rectTarget = target;
startPos.x = ev.clientX;
startPos.y = ev.clientY;
startPos.w = parseInt(rectTarget.style.width || 0, 10);
startPos.h = parseInt(rectTarget.style.height || 0, 10);
// 右下角 15 x 15 大小调整区
if (mouseX > eleRect.width - 15 && mouseY > eleRect.height - 15) {
let $eleRect = this.$thumbImageContainer.find('.fv__thumb-page .fv__thumb-view-rect.fv__rect_active');
let $eleThumb = $eleRect.parent().find('.fv__thumb-image');
$eleRect.css('max-width', $eleThumb.outerWidth() + RECT_BORDER_WIDTH)
.css('max-height', $eleThumb.outerHeight() + RECT_BORDER_WIDTH);
this.resizing = true;
return false;
}
this.moving = true;
elePos.x = parseInt(rectTarget.style.left || 0, 10);
elePos.y = parseInt(rectTarget.style.top || 0, 10);
return false;
}
});
!this.evDocUp && $(document).on('mouseup', this.evDocUp = ev => {
if (this.resizing || this.moving) {
let $eleRect = this.$thumbImageContainer.find('.fv__thumb-page .fv__thumb-view-rect.fv__rect_active');
$eleRect.css('max-width', '')
.css('max-height', '');
let isOnlyMoving = false;
ev.stopPropagation();
this.resizing = false;
if (this.moving && !this.resizing) isOnlyMoving = true;
this.rectZoomToPage(isOnlyMoving);
this.moving = false;
rectTarget = null;
}
});
!this.evDocMove && $(document).on('mousemove', this.evDocMove = ev => {
if (this.resizing) {
ev.preventDefault();
ev.stopPropagation();
let w = ev.clientX - startPos.x;
let h = ev.clientY - startPos.y;
rectTarget.style.width = (startPos.w + w) + 'px';
rectTarget.style.height = (startPos.h + h) + 'px';
}
if (this.moving && rectTarget) {
ev.preventDefault();
ev.stopPropagation();
let x = ev.clientX - startPos.x;
let y = ev.clientY - startPos.y;
let top = elePos.y + y;
let left = elePos.x + x;
let offsetLeft = thumbRect.left - parentRect.left - THUMB_BORDER_WIDTH;
let offsetTop = thumbRect.top - parentRect.top - THUMB_BORDER_WIDTH;
top = Math.min(Math.max(top, offsetTop - THUMB_BORDER_WIDTH), offsetTop + thumbRect.height - eleRect.height);
left = Math.min(Math.max(left, offsetLeft - THUMB_BORDER_WIDTH), offsetLeft + thumbRect.width - eleRect.width);
rectTarget.style.top = top + 'px';
rectTarget.style.left = left + 'px';
}
});
}
rectZoomToPage(isMoving = false) {
if (!isDesktop) return;
let pdfViewer = this.pdfUI.pdfViewer;
let pdfDocRender = pdfViewer.getPDFDocRender();
let scrollWrap = pdfDocRender.viewerRender.scrollWrap;
let pageIndex = pdfDocRender.getCurrentPageIndex();
let pdfPageRender = pdfViewer.getPDFPageRender(pageIndex);
let prevScale = pdfPageRender.getScale();
let $eleRect = this.$thumbImageContainer.find('.fv__thumb-page .fv__thumb-view-rect.fv__rect_active');
let $eleThumb = $eleRect.parent().find('.fv__thumb-image');
let $thumbPage = $eleThumb.parents('.fv__thumb-page');
let thumbPos = $eleThumb.position();
let rectPos = $eleRect.position();
// 可视框大小
let viewportSize = {
width: scrollWrap.getWidth(),
height: scrollWrap.getHeight()
};
// 缩略图大小
let thumbInfo = {
pos: {
x: $eleThumb.offset().left - $thumbPage.offset().left + THUMB_BORDER_WIDTH,
y: $eleThumb.offset().top - $thumbPage.offset().top + THUMB_BORDER_WIDTH
},
size: {
width: $eleThumb.outerWidth() - THUMB_BORDER_WIDTH,
height: $eleThumb.outerHeight() - THUMB_BORDER_WIDTH
}
};
let pdfPageSize = {
width: pdfPageRender.getShowWidth() / prevScale,
height: pdfPageRender.getShowHeight() / prevScale
};
let rectInfo = {
pos: {
x: rectPos.left - thumbInfo.pos.x + RECT_BORDER_WIDTH,
y: rectPos.top - thumbInfo.pos.y + RECT_BORDER_WIDTH
},
size: {
width: $eleRect.outerWidth() - RECT_BORDER_WIDTH * 2,
height: $eleRect.outerHeight() - RECT_BORDER_WIDTH * 2
}
};
let width = rectInfo.size.width;
let height = rectInfo.size.height;
// 宽高调整到可视窗口同比例
if (width / viewportSize.width < height / viewportSize.height) {
width = height * viewportSize.width / viewportSize.height;
}
let pdfShowWidth = width / thumbInfo.size.width * pdfPageSize.width;
let scale = viewportSize.width / pdfShowWidth;
let x = rectInfo.pos.x / thumbInfo.size.width * pdfPageSize.width * prevScale;
let y = rectInfo.pos.y / thumbInfo.size.height * pdfPageSize.height * prevScale;
if (isMoving) {
pdfViewer.zoomTo(undefined, {
x,
y,
pageIndex,
type:'device'
});
} else {
pdfViewer.zoomTo(scale, {
x: this.prevPos.x / prevScale * scale,
y: this.prevPos.y / prevScale * scale,
pageIndex,
type:'device'
});
}
}
// 计算缩略图中框
calcThumbnailRect(isPageChanged = false, newPageIndex = 0) {
if (!isDesktop) return;
let pageIndex = this.cachePageIndex || 0;
let $rects = this.$thumbImageContainer.find('.fv__thumb-view-rect');
if(!$rects.length)return;
if (isPageChanged || !$($rects[pageIndex]).hasClass('fv__rect_active')) {
$rects.hide().removeClass('fv__rect_active');
this.cachePageIndex = pageIndex = newPageIndex;
}
let $eleRect = $($rects[pageIndex]);
let $eleThumb = $eleRect.parent().find('.fv__thumb-image');
let $thumbPage = $eleThumb.parents('.fv__thumb-page');
let thumbPos = $eleThumb.position();
let pdfPageRender = this.pdfUI.pdfViewer.getPDFPageRender(pageIndex);
let thumbInfo = {
pos: {
x: $eleThumb.offset().left - $thumbPage.offset().left,
y: thumbPos.top
},
size: {
width: $eleThumb.width(),
height: $eleThumb.height()
}
};
let pageVisibleRect = pdfPageRender.getContainerVisibleRect();
let pdfPageInfo = {
visible: {
x: pageVisibleRect.left,
y: pageVisibleRect.top,
width: pageVisibleRect.right - pageVisibleRect.left,
height: pageVisibleRect.bottom - pageVisibleRect.top
},
size: {
width: pdfPageRender.getShowWidth(),
height: pdfPageRender.getShowHeight()
}
};
let x = thumbInfo.pos.x + pdfPageInfo.visible.x * (thumbInfo.size.width / pdfPageInfo.size.width);
let y = thumbInfo.pos.y + pdfPageInfo.visible.y * (thumbInfo.size.height / pdfPageInfo.size.height);
let width = pdfPageInfo.visible.width * (thumbInfo.size.width / pdfPageInfo.size.width);
let height = pdfPageInfo.visible.height * (thumbInfo.size.height / pdfPageInfo.size.height);
this.prevPos.x = pdfPageInfo.visible.x;
this.prevPos.y = pdfPageInfo.visible.y;
if (Number.isNaN(width) || width <= 0 || Number.isNaN(height) || height <= 0) return;
$eleRect.css('top', y - RECT_BORDER_WIDTH)
.css('left', x - RECT_BORDER_WIDTH)
.css('width', width + RECT_BORDER_WIDTH * 2)
.css('height', height + RECT_BORDER_WIDTH * 2)
.show()
.addClass('fv__rect_active');
isSafari && $eleRect.addClass('fv__thumb-view-safari');
}
updateVisiblePages(enforce) {
// if thumb container isn't visible, don't render it.
if(!$(this.element).is(':visible')) {
return;
}
// adjust container's flex layout strategy before update
adjustContainerFlexBox(this.$thumbImageContainer);
const $pages = this.$pages;
let visiblePages = [];
let visibleClientRect = this.$thumbImageContainer.parent()[0].getBoundingClientRect();
// Algorithm to calculate the thumbnails currently appearing in the visible area.
let currentPageIndex = this.getPDFUI().pdfViewer.getPDFDocRender().getCurrentPageIndex();
const pageCount = this.getPDFUI().pdfViewer.getCurrentPDFDoc().getPageCount();
if (currentPageIndex >= pageCount) {
currentPageIndex = pageCount - 1;
}
let i = currentPageIndex, j = currentPageIndex + 1;
while (i >= 0) {
let clientRect = $pages[i].getBoundingClientRect();
if (clientRect.bottom < visibleClientRect.top) {
break;
}
if (isRectsOverlap(clientRect, visibleClientRect)) {
visiblePages.unshift(i);
}
i--;
}
while (j < $pages.length) {
let clientRect = $pages[j].getBoundingClientRect();
if (clientRect.top > visibleClientRect.bottom) {
break;
}
if (isRectsOverlap(clientRect, visibleClientRect)) {
visiblePages.push(j);
}
j++;
}
const currentScaleRatio = this.zoomService.getScaleRatio();
// The current is the same as the last time.
if (!enforce
&& this.visiblePages.length === visiblePages.length
&& !this.visiblePages.reduce((pre, pageIdx, idx) => pre ^ pageIdx ^ visiblePages[idx], 0)
&& this.lastScaleRatio === currentScaleRatio) {
return;
}
this.visiblePages = visiblePages;
this.lastScaleRatio = currentScaleRatio;
const promises = visiblePages.map(i => {
// Update the currently visible thumbnail, because of enlarge/reduce feature.
return this.controller.updatePageThumbImg(i);
});
return Promise.all(promises);
}
// 插入缩略图
insertThumb(pageIndex, needGoto = true) {
let pageNum = pageIndex + 1;
let htmlTemplate = compileThumbTemplate(pageNum);
// change page width/height if scaleLevel not equal INITIALIZE value
if (!this.zoomService.scaleEqualInitialize()) {
let $htmlTemplate = $(htmlTemplate);
const width = THUMBNAIL_PAGE_BASIS_WIDTH * this.zoomService.zoomToScaleRatio;
const height = THUMBNAIL_PAGE_BASIS_HEIGHT * this.zoomService.zoomToScaleRatio;
$htmlTemplate.css({
width: `${width}px`,
height: `${height}px`
})
htmlTemplate = $htmlTemplate;
}
// replace pages 功能可能会删除完后再插入缩略图
if (this.$pages.length === 0) {
this.$thumbImageContainer.append(htmlTemplate);
}
if (pageIndex == this.$pages.length) {
$(this.$pages[pageIndex - 1]).after(htmlTemplate);
} else
$(this.$pages[pageIndex]).before(htmlTemplate);
this.$pages = this.$thumbImageContainer.find(".fv__thumb-page");
if (needGoto) {
this.updatePageLabel(pageIndex);
this.updateVisiblePages(true);
}
}
// 移除缩略图
removeThumb(index) {
let $pages = this.$pages;
if (!$pages) return;
$pages[index].remove();
this.$pages = this.$thumbImageContainer.find(".fv__thumb-page");
this.updatePageLabel(index);
this.updateVisiblePages(true);
}
// 载入图片
removeThumbs(pageIndexes){
pageIndexes.forEach(index=>{
let $pages = this.$pages;
if(!$pages)return;
$pages[index].remove();
})
this.$pages=this.$thumbImageContainer.find('.fv__thumb-page');
// pageIndexes.forEach(index=>this.updatePageLabel(index)) // PageLabel update by Event.pageLabelChange triggered
this.updateVisiblePages(true);
}
displayThumbImg(pageIndex, data) {
let $pageThumb = $(this.$pages[pageIndex]);
if($pageThumb.length < 1) {
return;
}
let eCanvas = $pageThumb.find(".fv__thumb-image")[0];
eCanvas.removeClass("fv__hide");
eCanvas.width = data.width;
eCanvas.height = data.height;
let $pageContainer = $pageThumb.find(".fv__thumb-page-container")
$pageContainer.addClass("loading");
const ratioCanvas = data.width / data.height;
const ratioContainer = $pageContainer.width() / $pageContainer.height();
if(ratioContainer !== 0) {
if(ratioCanvas < ratioContainer) {
const percentRatio = (ratioCanvas / ratioContainer) * 100;
eCanvas.style.maxWidth = percentRatio.toString() + "%";
eCanvas.style.maxHeight = "100%";
} else {
const percentRatio = (ratioContainer / ratioCanvas ) * 100;
eCanvas.style.maxHeight = percentRatio.toString() + "%";
eCanvas.style.maxWidth = "100%";
}
}
let ctx = eCanvas.getContext("2d");
if (data.buffer) {
let buffer = new (Uint8ClampedArray || Uint8Array)(data.buffer);
let img = ctx.createImageData(data.width, data.height);
img.data.set(buffer);
ctx.putImageData(img, 0, 0);
$pageContainer.removeClass("loading");
} else if (data.image) {
let img = new Image();
img.onload = function() {
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(img.src);
$pageContainer.removeClass("loading");
};
img.src = URL.createObjectURL(data.image);
}
if (pageIndex === this.cachePageIndex) this.calcThumbnailRect(true, pageIndex)
}
// 高亮右键菜单页 （已废弃）
highlightMenuTarget(pageIndex) {
let $thumbPages = this.$pages;
if ($thumbPages[pageIndex]) {
this.$html
.find(".fv__active_contextmenu")
.removeClass("fv__active_contextmenu");
let $activeThumb = $($thumbPages[pageIndex]);
$activeThumb.addClass("fv__active_contextmenu");
}
}
// 高亮选中页
highlightPage(pageIndex, enforce) {
this.getPDFUI().pdfViewer.keyboard.defaultDeleteBinderName = 'thumbUIComponent';
// fix: ONLINERD-2557
const activeEle = this.$html.find(".fv__active");
// fix: ONLINERD-5023(caused by fix ONLINERD-2557)
const hasCurActiveIndex = activeEle.length === 1 && (activeEle.data('page-index') !== pageIndex);
const newPageIndex = typeof this.fromClickThumb === 'number' ? this.fromClickThumb : pageIndex;
if (this.fromClickThumb !== pageIndex || hasCurActiveIndex) {
let thumbPages = this.$html.find(".fv__thumb-page");
this.$html.find(".fv__active").removeClass("fv__active");
let $activeThumb = $(thumbPages[newPageIndex]);
$activeThumb.addClass("fv__active");
let $parent = this.$thumbImageContainerBox;
const thumbTop = $parent.offset().top + 10;
let offset = $activeThumb.offset();
let top = offset && offset.top;
if (top && top !== thumbTop) {
let currentTop = $parent.scrollTop();
$parent.scrollTop(currentTop + top - thumbTop);
}
}
this.calcThumbnailRect(true, newPageIndex);
this.doPageAction(this.previePageIndex, pageIndex);
this.fromClickThumb = null;
this.previePageIndex = pageIndex;
this.updateVisiblePages(enforce);
}
// 执行page action, 需要执行当前页面的pageOpen action，和源页面pageclose action
doPageAction(fromPageIndex, targetPageIndex) {
if(fromPageIndex === targetPageIndex || fromPageIndex === undefined) return;
const selectedThumb = $('.fv__thumb-page.fv__active');
// 多选情况下不触发action
if(selectedThumb.length > 1) return;
// thumbnailClicked emitter
this.pdfUI.eventEmitter.emit(Events.thumbnailClicked, { fromPageIndex, targetPageIndex });
}
// 更新页码
async updatePageLabel(from, to) {
this.$pages = this.$thumbImageContainer.find(".fv__thumb-page");
let $pageThumbs = this.$pages;
if (typeof to === 'undefined') {
to = $pageThumbs.length;
}
const logicalPageNumChecked = isPageLabelSetting();
let hasLabel = false;
let labels = [];
if (logicalPageNumChecked) {
const labelsObj = await this._checkAndGetLabels();
hasLabel = labelsObj.hasLabel;
labels = labelsObj.labels;
}
for (let i = from; i <= to; i++) {
const label = logicalPageNumChecked && hasLabel ? htmlCharacter2Entity(labels[i]) : i + 1;
let $pageThumb = $($pageThumbs[i]);
let $pageIndexDiv = $pageThumb.find(".fv__thumb-page-index");
$pageIndexDiv.html(label);
$pageThumb.attr("data-page-index", i);
$pageThumb.data("page-index", i);
}
}
reset() {
const $rects = this.$thumbImageContainer.find('.fv__thumb-view-rect');
resetContainerStyle(this.$thumbImageContainer);
this.fromClickThumb = null;
$(document).off('mouseup', this.evDocUp);
$(document).off('mousemove', this.evDocMove);
$rects.off('mousedown', this.evTnRectDown);
this.evDocUp = null;
this.evDocMove = null;
this.evTnRectDown = null;
}
// 复制页面
copyPages(sourceIndexs, destIndex) {
// 构造新数组
const pages = [];
let tempArray = [];
sourceIndexs.forEach(item => {
if(tempArray.length > 0 && item !== tempArray[tempArray.length - 1] + 2) {
pages.push(tempArray);
tempArray = [];
}
if(tempArray.length > 1) {
tempArray[1] = item;
} else {
tempArray.push(item);
}
});
pages.push(tempArray);
this.controller.copyPages(pages, destIndex);
}
// 移动页面
movePages(sourceIndexs, destIndex) {
sourceIndexs = sourceIndexs.map(sourceIndex => {
return [sourceIndex];
})
const pdfDoc = this.getPDFUI().pdfViewer.currentPDFDoc;
pdfDoc.movePagesToThenUpdatePageLabel(sourceIndexs, destIndex);
}
// 移动页面(外部事件监听)
movePageImage(src, dst) {
if (this.moveImageDone) {
this.moveImageDone = false;
return;
}
let min = Math.min(src, dst);
let max = Math.max(src, dst);
this.$pages = this.$thumbImageContainer.find(".fv__thumb-page");
for (let i = min; i <= max; i++) {
let $pageThumb = this.$pages.eq(i).find('.fv__thumb-page-container');;
$pageThumb.addClass("loading");
}
this.updatePageLabel(min, max);
this.updateVisiblePages(true);
}
movePagesImage (pageRange, destIndex, pageIdList) {
const min = Math.min(...pageRange, destIndex);
const max = Math.max(...pageRange, destIndex)
this.$pages = this.$thumbImageContainer.find('.fv__thumb-page');
for (let i = min; i <= max; i++) {
let $pageThumb = this.$pages.eq(i).find('.fv__thumb-page-container');
$pageThumb.addClass("loading");
}
this.updateVisiblePages(true);
}
// 判断某个模块是否加载
checkAddonInstance (addonName) {
const addonMap = this.getPDFUI().addonInstanceMap || {};
return addonMap[addonName];
}
// 获取选中的页
getSelectedPages() {
const $activePages = this.$html.find(".fv__active");
let indexes = [];
$activePages.each((index, item) => {
indexes.push($(item).data("page-index"));
});
return indexes;
}
// 获取所有激活的页面下标, 返回格式：1,5-9,12
getAllActivePageIndex (exitIndexArray) {
let result = [];
let pageIndexs = exitIndexArray || [];
const $activePages = this.$html.find('.fv__active');
$activePages.each((index, target) => {
pageIndexs.push($(target).attr('data-page-index'));
});
// 去重、排序
pageIndexs = Array.from(new Set(pageIndexs)).sort((a, b) => a - b);
// 下标数组[1,2,3,4,5,6,10,11]循环成[1-6,10,11]
pageIndexs.reduce(function (a, b) {
if (!a || b - a > 1) {
result[result.length] = [+b + 1];
} else if (b - a === 1) {
result[result.length - 1].push(+b + 1);
}
return b;
}, '');
let ret = result.map(function (item) {
if (item.length === 1) return item[0];
return item[0] + '-' + item[item.length - 1];
});
return [result, ret];
}
get selectedPageRanges() {
const selectedPages = this.getSelectedPages();
const isSelected = selectedPages.length !== 0;
const currentPage =
this.getPDFUI()
.pdfViewer.getPDFDocRender()
.getCurrentPageIndex() + 1;
const pages = isSelected
? selectedPages.map(page => page + 1)
: [currentPage];
return {
...__.business.range_.getPageRanges(pages, 1),
isSelected
};
}
// 判断选中的页是否为连续页
checkContinuePage(ids) {
if(!Array.isArray(ids)) {
return false;
}
if(ids.length === 1) {
return true;
}
for (let i=0; i<ids.length; i++) {
if(i === ids.length - 1) {
return true;
}
if( ids[i] + 1 !== ids[i+1]) {
return false;
}
}
}
addDragEvent () {
const isCollaborationMode = this.pdfUI.customs && this.pdfUI.customs.isCollaborationMode();
if (isCollaborationMode || this.dragable) return ;
const e1 = this.$html.find(".fv__thumb-image-container")[0];
// 拖动时末尾的占位框
let dragLastNode = document.createElement("div");
dragLastNode.setAttribute("data-page-index","-1");
dragLastNode.style.borderStyle = "dashed";
this.dragable = Dragable.create(e1, {
draggable: "fv__thumb-page", // 允许拖拽的节点类名
dragClass: "draggable-drag", // 正在被拖拽中的css类名
chosenClass: "thumb-sortable-ghost", // 被选中项的css 类名
inventedNode: dragLastNode,
endStrict: false,
dragOverStrict: true,
checkIsMoveToRight: (target, info) => {
const targetRect = target.getBoundingClientRect();
const centerX = (targetRect.left + targetRect.right) * 0.5;
return info.mouseX > centerX;
},
// 隐藏子菜单
onStart: (data) => {
const dom = $(data.dom);
if(!dom.hasClass("fv__active")) {
this.$html.find(".fv__active").removeClass("fv__active");
dom.addClass("fv__active");
}
},
// 悬停打开子菜单
onHover: (data) => {
},
onDrop: (data) => {
if (this.dragable.options.allowDrag) {
const dragFiles = data.dragFiles;
// 如果 dragFiles length > 0则说明是文件拖入 insert
if (dragFiles.length === 0) {
return;
}
this.dragFileInsert(data);
} else {
this.dragable._removeClass(this.dragable.options.chosenClass, data.target);
}
},
onEnd: (data) => {
if(!data || !data.end) {
isTouchDevice && data?.fromAttrs?.pageIndex && (
this.getPDFUI().goToPage(parseInt(data.fromAttrs.pageIndex, 10), 0, 0, false)
);
return;
}
const $activePages = this.$html.find(".fv__active");
let ids = [];
$activePages.each((index, item) => {
ids.push($(item).data("page-index"));
});
if(ids.length < 1) {
return;
}
let toIndex = parseInt(data.endAttrs.pageIndex);
if (data.isMoveToRight) toIndex++;
const pageCnt = this.$pages.length;
if(toIndex === -1) {
toIndex = pageCnt;
}
if((isMacOS && data.end.altKey) || (!isMacOS && data.end.ctrlKey)) { // 复制
this.copyPages(ids, toIndex);
} else { // 移动
// 以下是移动位置的限制
if(ids.length === 1) {
if(ids[0] === toIndex || ids[0] === toIndex - 1) {
return;
}
}else {
if(this.checkContinuePage(ids)) {
if(ids.includes(toIndex) || ids[ids.length - 1] === toIndex - 1) {
return;
}
}
}
this.movePages(ids, toIndex);
}
}
});
this.addDestroyHook(() => {
this.removeDragEvent();
});
}
removeDragEvent () {
this.dragable && this.dragable.destory();
this.dragable = null;
}
}
const isRectsOverlap = (src, dst) => {
let left = Math.max(src.left, dst.left);
let top = Math.max(src.top, dst.top);
let bottom = Math.min(src.bottom, dst.bottom);
let right = Math.min(src.right, dst.right);
return right >= left && bottom >= top;
};

@2025 Foxit Software Incorporated. All rights reserved.