Преглед на файлове

Merge branch 'ui_customization'

liutian преди 1 година
родител
ревизия
feb43fdb26

+ 3 - 1
packages/core/webviewer.js

@@ -7,7 +7,9 @@ const ComPDFKitViewer = {
 
         Promise.resolve().then(function() {
           resolve({
-            docViewer: iframeWindow.instance
+            docViewer: iframeWindow.instance,
+            Core: iframeWindow.instances.Core,
+            UI: iframeWindow.instances.UI
           })
         })
 

+ 26 - 0
packages/webview/src/apis/disableElements.js

@@ -0,0 +1,26 @@
+// 隐藏工具栏按钮
+export default (store) => (dataElements) => {
+  const headerItems = [...store.getActiveHeaderItems, ...store.getTools, ...store.getActiveRightHeaderItems, ...store.getToolItems.annotation[0].tools, ...store.getToolItems.form];
+
+  if (Array.isArray(dataElements)) {
+    for (let i = 0; i < headerItems.length; i++) {
+      dataElements.forEach(element => {
+        if (headerItems[i].dataElement === element) {
+          headerItems[i].hidden = true;
+        }
+      });
+    }
+  } else if (typeof dataElements === 'string') {
+    const obj = headerItems.find(obj => obj.dataElement === dataElements);
+    if (obj) {
+      obj.hidden = true;
+      if (store.toolMode === obj.element) {
+        store.setActiceToolMode(store.tools[0]?.element);
+      }
+    } else {
+      console.warn('DataElement not found.');
+    }
+  } else {
+    console.error('Wrong dataElements type.');
+  }
+};

+ 23 - 0
packages/webview/src/apis/enableElements.js

@@ -0,0 +1,23 @@
+// 显示工具栏按钮
+export default (store) => (dataElements) => {
+  const headerItems = [...store.getActiveHeaderItems, ...store.getTools, ...store.getActiveRightHeaderItems, ...store.getToolItems.annotation[0].tools, ...store.getToolItems.form];
+
+  if (Array.isArray(dataElements)) {
+    for (let i = 0; i < headerItems.length; i++) {
+      dataElements.forEach(element => {
+        if (headerItems[i].dataElement === element) {
+          headerItems[i].hidden = false;
+        }
+      });
+    }
+  } else if (typeof dataElements === 'string') {
+    const obj = headerItems.find(obj => obj.dataElement === dataElements);
+    if (obj) {
+      obj.hidden = false;
+    } else {
+      console.warn('DataElement not found.');
+    }
+  } else {
+    console.error('Wrong dataElements type.');
+  }
+};

+ 44 - 0
packages/webview/src/apis/index.js

@@ -0,0 +1,44 @@
+import core from '@/core';
+import { useViewerStore } from '@/stores/modules/viewer';
+import { useDocumentStore } from '@/stores/modules/document';
+
+import disableElements from './disableElements';
+import enableElements from './enableElements';
+import setHeaderItems from './setHeaderItems';
+import setActiceToolMode from './setActiceToolMode';
+
+export default () => {
+  const useViewer = useViewerStore()
+  const useDocument = useDocumentStore()
+
+  const CORE_NAMESPACE = 'Core';
+  const UI_NAMESPACE = 'UI';
+  const objForWebViewerCore = {
+    instance: window.instance
+    // Tools: window.Core.Tools,
+    // Annotations: window.Core.Annotations,
+    // PartRetrievers: window.Core.PartRetrievers,
+    // Actions: window.Core.Actions,
+    // PDFNet: window.Core.PDFNet,
+  };
+  const objForWebViewerUI = {
+    disableElements: disableElements(useViewer),
+    enableElements: enableElements(useViewer),
+    setHeaderItems: setHeaderItems(useViewer),
+    setActiceToolMode: setActiceToolMode(useViewer),
+  }
+  const documentViewer = core.getDocumentViewer(1);
+
+  window.instances = {
+    // CORE_NAMESPACE_KEY: CORE_NAMESPACE,
+    // UI_NAMESPACE_KEY: UI_NAMESPACE,
+    [CORE_NAMESPACE]: {
+      ...objForWebViewerCore,
+      ...core,
+      documentViewer,
+      // annotationManager: documentViewer.getAnnotationManager(),
+      getDocumentViewers: () => core.getDocumentViewers(),
+    },
+    [UI_NAMESPACE]: objForWebViewerUI,
+  };
+}

+ 24 - 0
packages/webview/src/apis/setActiceToolMode.js

@@ -0,0 +1,24 @@
+
+/**
+ * Sets the current active toolbar group.
+ * @method UI.setActiceToolMode
+ * @param {string} groupDataElement The groups dataElement. Default values are: toolMenu-View, toolMenu-Annotation,
+ * toolMenu-Form, toolMenu-Sign, toolMenu-Security, toolMenu-Compare, toolMenu-Editor
+ * @example
+WebViewer(...)
+  .then(function(instance) {
+    // Change the toolbar group to the `Forms` group
+    instance.UI.setActiceToolMode('toolMenu-Form');
+ */
+
+export default (store) => (dataElement) => {
+  const tool = store.tools.find(tool => tool.dataElement === dataElement);
+  if (tool) {
+    if (tool.hidden) {
+      tool.hidden = false;
+    }
+    store.setActiceToolMode(tool.element);
+  } else {
+    console.warn('DataElement not found.');
+  }
+};

+ 327 - 0
packages/webview/src/apis/setHeaderItems.js

@@ -0,0 +1,327 @@
+/**
+ * Customize header. Refer to <a href='https://docs.apryse.com/documentation/web/guides/customizing-header/' target='_blank'>Customizing header</a> for details.
+ * @method UI.setHeaderItems
+ * @param {UI.headerCallback} headerCallback Callback function to perform different operations on the header.
+ * @example
+// Adding save annotations button to the end of the top header
+WebViewer(...)
+  .then(function(instance) {
+    instance.UI.setHeaderItems(function(header) {
+      var myCustomButton = {
+        type: 'actionButton',
+        img: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>',
+        onClick: function() {
+
+        }
+      }
+
+      header.push(myCustomButton);
+    });
+  });
+ * @example
+// Removing existing buttons from the top header
+WebViewer(...)
+  .then(function(instance) {
+    instance.UI.setHeaderItems(function(header) {
+      header.update([]);
+    });
+  });
+ * @example
+// Appending logo to the 'Annotate' toolbar group and shifting existing buttons to the right
+WebViewer(...)
+  .then(function(instance) {
+    instance.UI.setHeaderItems(function(header) {
+      header.getHeader('toolbarGroup-Annotate').unshift({
+        type: 'customElement',
+        render: function() {
+          var logo = document.createElement('img');
+          logo.src = '/logo.svg';
+          logo.style.width = '200px';
+          logo.style.marginLeft = '10px';
+          logo.style.cursor = 'pointer';
+          logo.onclick = function() {
+            window.open('https://www.apryse.com', '_blank');
+          }
+          return logo;
+        }
+      }, {
+        type: 'spacer'
+      });
+    });
+  });
+ * @example
+// Moving the line tool from the 'Shapes' toolbar group to the 'Annotate' toolbar group
+WebViewer(...)
+  .then(function(instance) {
+    instance.UI.setHeaderItems(function(header) {
+      header.getHeader('toolbarGroup-Annotate').push({ type: 'toolGroupButton', toolGroup: 'lineTools', dataElement: 'lineToolGroupButton', title: 'annotation.line' });
+      header.getHeader('toolbarGroup-Shapes').delete(6);
+    });
+  });
+ */
+
+/**
+ * Callback that gets passed to {@link UI.setHeaderItems setHeaderItems}.
+ * @callback UI.headerCallback
+ * @param {UI.Header} header Header instance with helper functions
+ */
+
+export default (store) => (callback) => {
+  const headerGroups = {
+    headers: store.headers,
+    rightHeaders: store.rightHeaders,
+    tools: store.tools,
+    toolItems: store.toolItems
+  };
+  // const headerGroups = Object.keys(store.header);
+  const header = Object.create(Header).initialize(store, headerGroups);
+
+  callback(header);
+  // headerGroups.forEach((headerGroup) => {
+    // store.dispatch(actions.setHeaderItems(headerGroup, [...header.headers[headerGroup]]));
+  //   console.log(headerGroup, [...header[headerGroup]])
+  // });
+  Object.keys(headerGroups).forEach((key) => {
+    const headerGroup = headerGroups[key];
+    store[key] = headerGroup;
+  });
+};
+
+/**
+ * A class which contains header APIs.<br/><br/>
+ * <span style="color: red; font-size: 1.2em; font-weight: bold">⚠</span> You must NOT instantiate this yourself. Access the header instance in {@link UI.setHeaderItems setHeaderItems} as follows:
+ * @name Header
+ * @memberof UI
+ * @class
+ * @example
+WebViewer(...)
+  .then(function(instance) {
+    instance.UI.setHeaderItems(function(header) {
+      // instance of Header is passed to the callback
+    });
+  });
+ */
+const Header = {
+  initialize(viewerState) {
+    // viewerState.initHeader()
+    // this.headers = viewerState.header;
+    this.headers = {
+      headers: viewerState.headers,
+      rightHeaders: viewerState.rightHeaders,
+      tools: viewerState.tools,
+      toolItems: viewerState.toolItems
+    };
+    this.toolButtonObjects = viewerState.toolItems;
+    this.headerGroup = 'headers';
+    this.index = -1;
+
+    return this;
+  },
+  /**
+   * Select a button from header to edit.
+   * @method UI.Header#get
+   * @param {string} dataElement data-element of the button.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#insertBefore insertBefore}, {@link UI.Header#insertAfter insertAfter} and {@link UI.Header#delete delete} to perform an operation on the button.
+   */
+  get(dataElement) {
+    if (this.index !== -1) {
+      // get(dataElement) has been called before so we need to reset this
+      const item = this.headers[this.headerGroup][this.index];
+      Object.keys(item).forEach((key) => {
+        delete this[key];
+      });
+    }
+
+    this._setIndex(dataElement);
+
+    if (this.index === -1) {
+      console.warn(`${dataElement} does not exist in ${this.headerGroup} header`);
+    } else {
+      const item = this.headers[this.headerGroup][this.index];
+      Object.keys(item).forEach((key) => {
+        this[key] = item[key];
+      });
+    }
+
+    return this;
+  },
+  /**
+   * Get all list of header items from a group selected from {@link UI.Header#getHeader getHeader}. By default, it returns the items from 'default' group.
+   * @method UI.Header#getItems
+   * @returns {Array.<object>} List of header item objects. You can edit it using normal array operations and update the whole header by passing it to {@link UI.Header#update update}.
+   */
+  getItems() {
+    return this.headers[this.headerGroup];
+  },
+  /**
+   * Select a header group to edit.
+   * @method UI.Header#getHeader
+   * @param {string} headerGroup Name of the header group. Possible options are 'default', 'small-mobile-more-buttons', 'toolbarGroup-View', 'toolbarGroup-Annotate', 'toolbarGroup-Shapes', 'toolbarGroup-Insert', 'toolbarGroup-Measure', and 'toolbarGroup-Edit'
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  getHeader(headerGroup) {
+    const headerGroups = Object.keys(this.headers);
+
+    if (headerGroups.includes(headerGroup)) {
+      this.headerGroup = headerGroup;
+      this._resetIndex();
+    } else {
+      console.warn(`Header must be one of: ${headerGroups.join(' or ')}.`);
+    }
+
+    return this;
+  },
+  /**
+   * Insert a button before the selected button from {@link UI.Header#get get}.
+   * @method UI.Header#insertBefore
+   * @param {object} obj A header object. See <a href='https://docs.apryse.com/documentation/web/guides/customizing-header/#header-items' target='_blank'>Header items</a> for details.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  insertBefore(newItem) {
+    if (this.index === -1) {
+      console.warn('Please use .get(dataElement) first before using insertBefore');
+    } else {
+      this.headers[this.headerGroup].splice(this.index, 0, newItem);
+    }
+
+    return this;
+  },
+  /**
+   * Insert a button after the selected button from {@link UI.Header#get get}.
+   * @method UI.Header#insertAfter
+   * @param {object} obj A header object. See <a href='https://docs.apryse.com/documentation/web/guides/customizing-header/#header-items' target='_blank'>Header items</a> for details.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  insertAfter(newItem) {
+    if (this.index === -1) {
+      console.warn('Please use .get(dataElement) first before using insertAfter');
+    } else {
+      this.index++;
+      this.headers[this.headerGroup].splice(this.index, 0, newItem);
+    }
+
+    return this;
+  },
+  /**
+   * Delete a button.
+   * @method UI.Header#delete
+   * @param {(number|string)} [id] You can either pass an index or `data-element` of the button to delete. If you already selected a button from {@link UI.Header#get get}, passing null would delete the selected button.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  delete(arg) {
+    let index;
+
+    if (typeof arg === 'number') {
+      index = arg;
+    } else if (typeof arg === 'string') {
+      if (this._getIndexOfElement(arg) === -1) {
+        console.warn(`${arg} does not exist in ${this.headerGroup} header`);
+      } else {
+        index = this._getIndexOfElement(arg);
+      }
+    } else if (typeof arg === 'undefined') {
+      if (this.index === -1) {
+        console.warn('Please use .get(dataElement) first before using delete()');
+      } else {
+        index = this.index;
+      }
+    } else if (Array.isArray(arg)) {
+      arg.forEach((arg) => {
+        if (typeof arg === 'number' || typeof arg === 'string') {
+          this.delete(arg);
+        }
+      });
+    } else {
+      console.warn('Argument must be empty, a number, a string or an array');
+    }
+
+    if (index !== undefined && index !== -1) {
+      this.headers[this.headerGroup].splice(index, 1);
+      this._resetIndex();
+    }
+
+    return this;
+  },
+  /**
+   * Removes the first button in the header.
+   * @method UI.Header#shift
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  shift() {
+    this.headers[this.headerGroup].shift();
+
+    return this;
+  },
+  /**
+   * Adds a button (or buttons) to the beginning of the header.
+   * @method UI.Header#unshift
+   * @param {object|Array.<object>} obj Either one or array of header objects. See <a href='https://docs.apryse.com/documentation/web/guides/customizing-header/#header-items' target='_blank'>Header items</a> for details.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  unshift(...newItem) {
+    this.headers[this.headerGroup].unshift(...newItem);
+
+    return this;
+  },
+  /**
+   * Adds a button (or buttons) to the end of the header.
+   * @method UI.Header#push
+   * @param {object|Array.<object>} obj Either one or array of header objects. See <a href='https://docs.apryse.com/documentation/web/guides/customizing-header/#header-items' target='_blank'>Header items</a> for details.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  push(...newItem) {
+    this.headers[this.headerGroup].push(...newItem);
+
+    return this;
+  },
+  /**
+   * Removes the last button in the header.
+   * @method UI.Header#pop
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  pop() {
+    this.headers[this.headerGroup].pop();
+
+    return this;
+  },
+  /**
+   * Updates the header with new list of header items.
+   * @method UI.Header#update
+   * @param {Array.<object>} headerObjects List of header objects to replace the exising header. You can use {@link UI.Header#getItems getItems} to refer to existing header objects.
+   * @returns {UI.Header} Header object for chaining. You can call {@link UI.Header#get get}, {@link UI.Header#getItems getItems}, {@link UI.Header#shift shift}, {@link UI.Header#unshift unshift}, {@link UI.Header#push push}, {@link UI.Header#pop pop} and {@link UI.Header#update update}.
+   */
+  update(arg) {
+    if (Array.isArray(arg)) {
+      this._updateItems(arg);
+    } else {
+      console.warn('Argument must be an array');
+    }
+
+    return this;
+  },
+  _updateItems(items) {
+    this.headers[this.headerGroup].length = 0
+    this.headers[this.headerGroup].push(...items)
+
+    return this;
+  },
+  _setIndex(dataElement) {
+    this.index = this._getIndexOfElement(dataElement);
+  },
+  _getIndexOfElement(dataElement) {
+    return this.headers[this.headerGroup].findIndex((item) => {
+      let dataElementOfItem;
+
+      if (item.type === 'toolButton') {
+        dataElementOfItem = this.toolButtonObjects[item.toolName].dataElement;
+      } else {
+        dataElementOfItem = item.dataElement;
+      }
+
+      return dataElementOfItem === dataElement;
+    });
+  },
+  _resetIndex() {
+    this.index = -1;
+  },
+};

+ 34 - 32
packages/webview/src/components/Annotate/Annotate.vue

@@ -1,40 +1,42 @@
 <template>
-  <StickyNoteButton />
-  <div class="markup-container">
-    <Button :class="{ active: markupActive && markupTool === 'highlight' }" @click.stop="changeMarkupTool('highlight')" :title="$t('header.highlight')"><Highlight /></Button>
-    <Button :class="{ active: markupActive && markupTool === 'underline' }" @click.stop="changeMarkupTool('underline')" :title="$t('header.underline')"><Underline /></Button>
-    <Button :class="{ active: markupActive && markupTool === 'strikeout' }" @click.stop="changeMarkupTool('strikeout')" :title="$t('header.strikeout')"><Strikeout /></Button>
-    <Button :class="{ active: markupActive && markupTool === 'squiggly' }" @click.stop="changeMarkupTool('squiggly')" :title="$t('header.squiggly')"><Squiggle /></Button>
-  </div>
-  <Button :class="{ active: activeTool === 'ink' }" @click="changeActiveTool('ink')" :title="$t('header.ink')">
-    <Ink />
-  </Button>
-  <div class="shape-container">
-    <Button :class="{ active: shapeActive && shapeTool === 'circle' }" @click.stop="changeShapeTool('circle')" :title="$t('header.circle')">
-      <EllipseTool />
+  <template v-for="(tool, index) in item" :key="`${tool.type}-${tool.dataElement || index}`">
+    <StickyNoteButton v-if="tool.type === 'note' && !tool.hidden" :data-element="tool.dataElement" />
+    <div v-else-if="['highlight', 'underline', 'strikeout', 'squiggly'].includes(tool.type)" class="markup-container">
+      <Button v-if="tool.type === 'highlight' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: markupActive && markupTool === 'highlight' }" @click.stop="changeMarkupTool('highlight')" :title="$t('header.highlight')"><Highlight /></Button>
+      <Button v-if="tool.type === 'underline' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: markupActive && markupTool === 'underline' }" @click.stop="changeMarkupTool('underline')" :title="$t('header.underline')"><Underline /></Button>
+      <Button v-if="tool.type === 'strikeout' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: markupActive && markupTool === 'strikeout' }" @click.stop="changeMarkupTool('strikeout')" :title="$t('header.strikeout')"><Strikeout /></Button>
+      <Button v-if="tool.type === 'squiggly' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: markupActive && markupTool === 'squiggly' }" @click.stop="changeMarkupTool('squiggly')" :title="$t('header.squiggly')"><Squiggle /></Button>
+    </div>
+    <Button v-if="tool.type === 'note' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: activeTool === 'ink' }" @click="changeActiveTool('ink')" :title="$t('header.ink')">
+      <Ink />
     </Button>
-    <Button :class="{ active: shapeActive && shapeTool === 'square' }" @click.stop="changeShapeTool('square')" :title="$t('header.square')">
-      <RectangleTool />
+    <div v-else-if="['circle', 'square', 'arrow', 'line'].includes(tool.type)" class="shape-container">
+      <Button v-if="tool.type === 'circle' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: shapeActive && shapeTool === 'circle' }" @click.stop="changeShapeTool('circle')" :title="$t('header.circle')">
+        <EllipseTool />
+      </Button>
+      <Button v-if="tool.type === 'square' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: shapeActive && shapeTool === 'square' }" @click.stop="changeShapeTool('square')" :title="$t('header.square')">
+        <RectangleTool />
+      </Button>
+      <Button v-if="tool.type === 'arrow' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: shapeActive && shapeTool === 'arrow' }" @click.stop="changeShapeTool('arrow')" :title="$t('header.arrow')">
+        <ArrowTool />
+      </Button>
+      <Button v-if="tool.type === 'line' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: shapeActive && shapeTool === 'line' }" @click.stop="changeShapeTool('line')" :title="$t('header.line')">
+        <LineTool />
+      </Button>
+    </div>
+    <Button v-else-if="tool.type === 'freetext' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: activeTool === 'freetext' }" @click="changeActiveTool('freetext')" :title="$t('header.freetext')">
+      <Text />
     </Button>
-    <Button :class="{ active: shapeActive && shapeTool === 'arrow' }" @click.stop="changeShapeTool('arrow')" :title="$t('header.arrow')">
-      <ArrowTool />
+    <Button v-else-if="tool.type === 'stamp' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: activeTool === 'stamp' }" @click="changeActiveTool('stamp')" :title="$t('header.stamp')">
+      <Stamp />
     </Button>
-    <Button :class="{ active: shapeActive && shapeTool === 'line' }" @click.stop="changeShapeTool('line')" :title="$t('header.line')">
-      <LineTool />
+    <Button v-else-if="tool.type === 'image' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: activeTool === 'image' }" @click="changeActiveTool('image')" :title="$t('header.image')">
+      <Image />
     </Button>
-  </div>
-  <Button :class="{ active: activeTool === 'freetext' }" @click="changeActiveTool('freetext')" :title="$t('header.freetext')">
-    <Text />
-  </Button>
-  <Button :class="{ active: activeTool === 'stamp' }" @click="changeActiveTool('stamp')" :title="$t('header.stamp')">
-    <Stamp />
-  </Button>
-  <Button :class="{ active: activeTool === 'image' }" @click="changeActiveTool('image')" :title="$t('header.image')">
-    <Image />
-  </Button>
-  <Button :class="{ active: activeTool === 'link' }" @click="changeActiveTool('link')" :title="$t('header.link')">
-    <Link />
-  </Button>
+    <Button v-else-if="tool.type === 'link' && !tool.hidden" :data-element="tool.dataElement" :class="{ active: activeTool === 'link' }" @click="changeActiveTool('link')" :title="$t('header.link')">
+      <Link />
+    </Button>
+  </template>
   <div class="divider pc"></div>
   <div v-if="showColors.includes(activeTool)" class="colors-container">
     <span class="cell-container">

+ 53 - 0
packages/webview/src/components/CustomButton/ActionButton.vue

@@ -0,0 +1,53 @@
+<template>
+  <div
+    class="button custom-button"
+    :data-element="dataElement"
+    :class="{
+      [type]: type,
+      disabled,
+      [className]: className
+    }"
+    @click="disabled || !onClick ? NOOP() : onClick($event)"
+    @dblclick="disabled ? NOOP : onDoubleClick"
+    @mouseup="disabled ? NOOP : onDoubleClick"
+    :disabled="disabled"
+    :title="title"
+  >
+    <template v-if="img">
+      <div v-if="isSVG" v-html="img" class="img"></div>
+      <img v-else :src="img" alt="" class="img">
+    </template>
+    <span v-if="item.dropItem" class="text">{{ item.text }}</span>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import { computed } from 'vue'
+import core from '@/core'
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const { item } = defineProps(['item'])
+const {
+  type,
+  img,
+  dataElement,
+  onClick,
+  title,
+  disabled,
+  className
+} = item
+
+const isSVG = img?.trim().toLowerCase().startsWith('<svg');
+
+const NOOP = (e) => {
+  e?.stopPropagation()
+  e?.preventDefault()
+}
+</script>
+
+<style lang="scss">
+</style>

+ 130 - 0
packages/webview/src/components/CustomButton/CToolButton.vue

@@ -0,0 +1,130 @@
+<template>
+  <div
+    class="button custom-button"
+    :class="{
+      active: isActive,
+      [type]: type,
+      [className]: className,
+      'with-text': forms.includes(toolName)
+    }"
+    @click="onClick()"
+    :title="title"
+  >
+    <Highlight v-if="toolName === 'highlight'" />
+    <Underline v-if="toolName === 'underline'" />
+    <Strikeout v-if="toolName === 'strikeout'" />
+    <Squiggle v-if="toolName === 'squiggly'" />
+    <Ink v-if="toolName === 'ink'" />
+    <EllipseTool v-if="toolName === 'circle'" />
+    <RectangleTool v-if="toolName === 'square'" />
+    <ArrowTool v-if="toolName === 'arrow'" />
+    <LineTool v-if="toolName === 'line'" />
+    <Text v-if="toolName === 'freetext'" />
+    <Stamp v-if="toolName === 'stamp'" />
+    <Image v-if="toolName === 'image'" />
+    <Link v-if="toolName === 'link'" />
+    
+    <template v-if="toolName === 'textfield'">
+      <TextFieldIcon />
+      <span>{{ $t('header.textField') }}</span>
+    </template>
+    <template v-if="toolName === 'checkbox'">
+      <CheckBoxIcon />
+      <span>{{ $t('header.checkbox') }}</span>
+    </template>
+    <template v-if="toolName === 'radiobutton'">
+      <RadioButtonIcon />
+      <span>{{ $t('header.radioButton') }}</span>
+    </template>
+    <template v-if="toolName === 'listbox'">
+      <ListBoxIcon />
+      <span>{{ $t('header.listBox') }}</span>
+    </template>
+    <template v-if="toolName === 'combobox'">
+      <ComboBoxIcon />
+      <span>{{ $t('header.comboButton') }}</span>
+    </template>
+    <template v-if="toolName === 'pushbutton'">
+      <PushButtonIcon />
+      <span>{{ $t('header.button') }}</span>
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import { computed } from 'vue'
+import core from '@/core'
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const { item } = defineProps(['item'])
+const {
+  type,
+  title,
+  disabled,
+  className,
+  toolName
+} = item
+
+const markups = ['highlight', 'underline', 'squiggly', 'strikeout']
+const shapes = ['square', 'circle', 'arrow', 'line']
+const annotations = ['ink', 'freetext', 'stamp', 'image', 'link']
+const forms = ['textfield', 'checkbox', 'radiobutton', 'listbox', 'combobox', 'pushbutton']
+const toolType = computed(() => {
+  if (markups.includes(toolName)) return 'markup'
+  if (shapes.includes(toolName)) return 'shape'
+  if (annotations.includes(toolName)) return 'annotation'
+  if (forms.includes(toolName)) return 'form'
+})
+
+const activeTool = computed(() => useDocument.getActiveTool)
+
+const isActive = computed(() => {
+  return toolName === activeTool.value
+})
+
+const changeActiveTool = (tool) => {
+  useDocument.setToolState(tool)
+  
+  useViewer.toggleActiveHand(false)
+  core.switchTool(0)
+  
+  core.switchAnnotationEditorMode(0)
+
+  if (tool === 'stamp') {
+    useViewer.toggleElement('stampPanel')
+  } else {
+    useViewer.closeElement('stampPanel')
+  }
+
+  if (activeTool.value === 'image') {
+    document.getElementById('signImageInput').click()
+  }
+}
+
+const changeMarkupTool = (tool) => {
+  useDocument.setMarkupToolState(tool)
+  changeActiveTool(tool)
+}
+
+const changeShapeTool = (tool) => {
+  useDocument.setShapeToolState(tool)
+  changeActiveTool(tool)
+}
+
+const onClick = (() => {
+  if (markups.includes(toolName)) changeMarkupTool(toolName)
+  if (shapes.includes(toolName)) changeShapeTool(toolName)
+  if (annotations.includes(toolName)) changeActiveTool(toolName)
+  if (forms.includes(toolName)) changeActiveTool(toolName)
+})
+</script>
+
+<style lang="scss" scoped>
+.custom-button {
+  width: auto;
+}
+</style>

+ 28 - 0
packages/webview/src/components/CustomButton/CustomButton.vue

@@ -0,0 +1,28 @@
+<template>
+  <ActionButton v-if="type === 'actionButton'" :item="item" />
+  <StatefulButton v-if="type === 'statefulButton'" :item="item" />
+  <ToggleButton v-if="type === 'toggleButton'" :item="item" />
+  <CToolButton v-if="type === 'toolButton'" :item="item" />
+</template>
+
+<script setup>
+const { item } = defineProps(['item'])
+const { type } = item
+</script>
+
+<style lang="scss">
+.custom-button {
+  min-width: 30px;
+  height: 30px;
+  .img {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 20px;
+    height: 20px;
+  }
+  .text {
+    margin-left: 8px;
+  }
+}
+</style>

+ 95 - 0
packages/webview/src/components/CustomButton/StatefulButton.vue

@@ -0,0 +1,95 @@
+<template>
+  <div
+    class="button custom-button"
+    :data-element="dataElement"
+    :class="{
+      active: isActive,
+      [type]: type,
+      disabled,
+      [className]: className
+    }"
+    @click="disabled || !onClick ? NOOP() : onClick($event)"
+    @dblclick="disabled ? NOOP : onDoubleClick"
+    @mouseup="disabled ? NOOP : onDoubleClick"
+    :disabled="disabled"
+    :title="title"
+  >
+    <template v-if="img">
+      <div v-if="isSVG" v-html="img" class="img"></div>
+      <img v-else :src="img" alt="" class="img">
+    </template>
+    {{ content }}
+    <span v-if="item.dropItem" class="text">{{ item.text }}</span>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, computed } from 'vue';
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import core from '@/core'
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const { item } = defineProps(['item'])
+const {
+  type,
+  dataElement,
+  title,
+  disabled,
+  className,
+
+  initialState,
+  states,
+  mount,
+  unmount,
+} = item
+
+const NOOP = (e) => {
+  e?.stopPropagation()
+  e?.preventDefault()
+}
+
+// statefulButton 有状态按钮
+const activeState = ref(initialState);
+const stateOptions = states[activeState.value];
+const isActive = ref(false);
+
+const { img } = stateOptions;
+const isSVG = img?.trim().toLowerCase().startsWith('<svg');
+
+const onClick = () => {
+  updateState();
+  stateOptions.onClick(stateOptions);
+};
+
+const updateState = () => {
+  activeState.value = initialState;
+};
+
+const content = computed(() => {
+  if (stateOptions.getContent instanceof Function) {
+    return stateOptions.getContent(stateOptions);
+  } else {
+    return stateOptions.getContent;
+  }
+});
+
+if (mount) {
+  onMounted(() => {
+    mount();
+  });
+}
+
+if (unmount) {
+  onUnmounted(() => {
+    unmount();
+  });
+}
+</script>
+
+<style lang="scss">
+.statefulButton {
+  background-color: var(--c-right-side-info-tip-bg);
+}
+</style>

+ 59 - 0
packages/webview/src/components/CustomButton/ToggleButton.vue

@@ -0,0 +1,59 @@
+<template>
+  <div
+    class="button custom-button"
+    :data-element="dataElement"
+    :class="{
+      active: isActive,
+      [type]: type,
+      disabled,
+      [className]: className
+    }"
+    @click="disabled || !onClick ? NOOP() : onClick($event)"
+    :disabled="disabled"
+    :title="title"
+  >
+    <template v-if="img">
+      <div v-if="isSVG" v-html="img" class="img"></div>
+      <img v-else :src="img" alt="" class="img">
+    </template>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import { useViewerStore } from '@/stores/modules/viewer'
+import { useDocumentStore } from '@/stores/modules/document'
+import { computed } from 'vue'
+import core from '@/core'
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const { item } = defineProps(['item'])
+const {
+  type,
+  img,
+  dataElement,
+  title,
+  disabled,
+  className,
+  element
+} = item
+
+const isSVG = img?.trim().toLowerCase().startsWith('<svg');
+
+const NOOP = (e) => {
+  e?.stopPropagation()
+  e?.preventDefault()
+}
+
+const isActive = computed(() => {
+  return useViewer.isElementOpen(element)
+})
+
+const onClick = () => {
+  useViewer.toggleElement(element)
+}
+</script>
+
+<style lang="scss">
+</style>

+ 1 - 1
packages/webview/src/components/DocumentContainer/DocumentContainer.vue

@@ -191,7 +191,7 @@ window.instance.initOptions = async (options) => {
     toggleButton: document.querySelector('.toggle-button')
   })
   let initialDoc = getHashParameters('d', '')
-  initialDoc = initialDoc ? JSON.parse(initialDoc) : ''
+  initialDoc = initialDoc ? JSON.parse(initialDoc) : '/example/Quick Start Guide for ComPDFKit Web Demo.pdf'
   initialDoc = Array.isArray(initialDoc) ? initialDoc : [initialDoc]
   const activeTab = useViewer.activeTab || 0
   initialDoc = initialDoc[activeTab]

+ 20 - 9
packages/webview/src/components/Dropdown/Dropdown.vue

@@ -15,15 +15,18 @@
       </Button>
     </template>
     <div class="drop-down">
-      <div class="drop-item" @click="downloadFile"><DownloadButton />{{ $t('header.download') }}</div>
-      <div class="drop-item" @click="handleFlatten"><FlattenButton />{{ $t('header.flatten') }}</div>
-      <div class="drop-item" @click="handlePrint"><PrintButton />{{ $t('header.print') }}</div>
-      <div class="drop-item" @click="handleLanguage">
-        <Button :title="$t('header.language')">
-          <Language />
-        </Button>
-        {{ $t('header.language') }}
-      </div>
+      <template v-for="(item, index) in rightItems" :key="`${item.type}-${item.dataElement || index}`">
+        <div v-if="item.type === 'downloadButton' && !item.hidden" :data-element="item.dataElement" class="drop-item" @click="downloadFile"><DownloadButton />{{ $t('header.download') }}</div>
+        <div v-if="item.type === 'flattenButton' && !item.hidden" :data-element="item.dataElement" class="drop-item" @click="handleFlatten"><FlattenButton />{{ $t('header.flatten') }}</div>
+        <div v-if="item.type === 'printButton' && !item.hidden" :data-element="item.dataElement" class="drop-item" @click="handlePrint"><PrintButton />{{ $t('header.print') }}</div>
+        <div v-if="item.type === 'languageButton' && !item.hidden" :data-element="item.dataElement" class="drop-item" @click="handleLanguage">
+          <Button :title="$t('header.language')"><Language /></Button>
+          {{ $t('header.language') }}
+        </div>
+        <div v-if="item.name === 'customButton' && item.dropItem && !item.hidden" :data-element="item.dataElement" class="drop-item">
+          <CustomButton :item="item" />
+        </div>
+      </template>
     </div>
   </n-popover>
 </template>
@@ -34,6 +37,7 @@
   import core from '@/core'
   import { useViewerStore } from '@/stores/modules/viewer'
   
+  const prop = defineProps(['rightItems'])
   const useViewer = useViewerStore()
   
   const popover = ref(null)
@@ -95,3 +99,10 @@
     useViewer.openElement('languageDialog')
   }
 </script>
+
+<style>
+.drop-item .custom-button {
+  width: 100%;
+  justify-content: flex-start;
+}
+</style>

+ 38 - 40
packages/webview/src/components/HeaderItems/HeaderItems.vue

@@ -1,13 +1,14 @@
 <template>
   <div v-if="toolMode !== 'compare'" class="header-items">
     <template v-for="(item, index) in items" :key="`${item.type}-${item.dataElement || index}`">
-      <ToggleElementButton v-if="item.type === 'toggleElementButton'" :item="item" :class="{ disabled: !load }" />
-      <ToolButton v-else-if="item.type === 'toolButton'" :item="item" />
-      <FullScreenButton v-else-if="item.type === 'fullScreenButton'" :item="item" :class="{ disabled: !load }" />
-      <HandButton v-else-if="item.type === 'handToolButton'" :item="item" :class="{ disabled: !load }" />
-      <ZoomOverlay v-else-if="item.type === 'zoomOverlay'" />
-      <ThemeMode v-else-if="item.type === 'themeMode'" />
-      <ViewRotationControls v-else-if="item.type === 'viewRotationControls'" />
+      <CustomButton v-if="item.name === 'customButton' && !item.hidden && !item.dropItem" :item="item" />
+      <ToggleElementButton v-else-if="item.type === 'toggleElementButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
+      <ToolButton v-else-if="item.type === 'toolButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <FullScreenButton v-else-if="item.type === 'fullScreenButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
+      <HandButton v-else-if="item.type === 'handToolButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
+      <ZoomOverlay v-else-if="item.type === 'zoomOverlay' && !item.hidden" :data-element="item.dataElement" />
+      <ThemeMode v-else-if="item.type === 'themeMode' && !item.hidden" :data-element="item.dataElement" />
+      <ViewRotationControls v-else-if="item.type === 'viewRotationControls' && !item.hidden" :data-element="item.dataElement" />
       <div v-else-if="['spacer', 'divider'].includes(item.type)" :class="item.type"></div>
     </template>
 
@@ -28,43 +29,49 @@
           </div>
         </template>
         <div class="drop-down narrower">
-          <div class="drop-item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
-          <div class="drop-item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
+          <template v-for="(item, index) in tools" :key="`${item.dataElement || index}`">
+            <div v-if="item.element === 'view' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
+            <div v-else-if="item.element === 'annotation' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
+            <div v-else-if="item.element === 'form' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
+            <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
+            <div v-else-if="item.element === 'security' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
+            <div v-else-if="item.element === 'compare' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
+            <div v-else-if="item.element === 'editor' && !item.hidden" :data-element="item.dataElement" class="drop-item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
+          </template>
         </div>
       </n-popover>
     </div>
     <div class="tool-container pc" :class="{ 'events-none': !load }">
       <div class="tool-menu">
-        <div class="item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
-        <div class="item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
-        <div class="item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
-        <div class="item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
-        <div class="item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
-        <div class="item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
-        <div class="item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
+        <template v-for="(item, index) in tools" :key="`${item.dataElement || index}`">
+          <div v-if="item.element === 'view' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'view' }" @click="changeToolMode('view')">{{ $t('header.viewer') }}</div>
+          <div v-else-if="item.element === 'annotation' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'annotation' }" @click="changeToolMode('annotation')">{{ $t('header.annotations') }}</div>
+          <div v-else-if="item.element === 'form' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'form' }" @click="changeToolMode('form')">{{ $t('header.forms') }}</div>
+          <div v-else-if="item.element === 'sign' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'sign' }" @click="openSignCreatePanel()">{{ $t('header.signatures') }}</div>
+          <div v-else-if="item.element === 'security' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'security' }" @click="changeToolMode('security')">{{ $t('header.security') }}</div>
+          <div v-else-if="item.element === 'compare' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'compare' }" @click="changeToolMode('compare')">{{ $t('header.compare') }}</div>
+          <div v-else-if="item.element === 'editor' && !item.hidden" :data-element="item.dataElement" class="item" :class="{ active: toolMode === 'editor' }" @click="changeToolMode('editor')">{{ $t('header.editor') }}</div>
+        </template>
       </div>
     </div>
 
     <div class="right-container">
       <template v-for="(item, index) in rightItems" :key="`${item.type}-${item.dataElement || index}`">
-        <SearchButton v-if="item.type === 'searchButton'" :item="item" />
-        <ToggleRightPanelButton v-if="item.type === 'toggleRightPanelButton'" :item="item" :disabled="!load" />
-        <!-- <OpenFileButton v-if="item.type === 'openFileButton'" :item="item" /> -->
+        <CustomButton v-if="item.name === 'customButton' && !item.hidden && !item.dropItem" :item="item" :data-element="item.dataElement" />
+        <SearchButton v-else-if="item.type === 'searchButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
+        <ToggleRightPanelButton v-else-if="item.type === 'toggleRightPanelButton' && !item.hidden" :item="item" :disabled="!load" :data-element="item.dataElement" />
+        <!-- <OpenFileButton v-else-if="item.type === 'openFileButton' && !item.hidden" :item="item" :data-element="item.dataElement" /> -->
+        <PageMode v-else-if="item.type === 'pageModeButton' && !item.hidden" :class="{ disabled: !load }" :data-element="item.dataElement" />
+        <div v-else-if="['spacer', 'divider'].includes(item.type)" :class="item.type"></div>
       </template>
-      <PageMode :class="{ disabled: !load }" />
-      <Dropdown :class="{ disabled: !load }" />
+      <Dropdown :class="{ disabled: !load }" :rightItems="rightItems" />
     </div>
   </div>
 
   <!-- 文档对比模式 -->
   <div v-else class="header-items">
     <template v-for="(item, index) in items" :key="`${item.type}-${item.dataElement || index}`">
-      <FullScreenButton v-if="item.type === 'fullScreenButton'" :item="item" :class="{ disabled: compareStatus !== 'finished' }" />
+      <FullScreenButton v-if="item.type === 'fullScreenButton' && !item.hidden" :item="item" :class="{ disabled: compareStatus !== 'finished' }" :data-element="item.dataElement" />
     </template>
     <div class="divider"></div>
 
@@ -72,15 +79,15 @@
 
     <div class="right-container">
       <template v-for="(item, index) in rightItems" :key="`${item.type}-${item.dataElement || index}`">
-        <DownloadButton v-if="item.type === 'downloadButton'" :item="item" :class="{ disabled: compareStatus !== 'finished' && compareStatus !== 'next' }" />
-        <PrintButton v-if="item.type === 'printButton'" :item="item" :class="{ disabled: compareStatus !== 'finished' && compareStatus !== 'next' }" />
+        <DownloadButton v-if="item.type === 'downloadButton' && !item.hidden" :item="item" :class="{ disabled: compareStatus !== 'finished' && compareStatus !== 'next' }" :data-element="item.dataElement" />
+        <PrintButton v-if="item.type === 'printButton' && !item.hidden" :item="item" :class="{ disabled: compareStatus !== 'finished' && compareStatus !== 'next' }" :data-element="item.dataElement" />
       </template>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, computed, getCurrentInstance } from 'vue'
+import { ref, computed, getCurrentInstance, watch } from 'vue'
 import { useViewerStore } from '@/stores/modules/viewer'
 import { useDocumentStore } from '@/stores/modules/document'
 import { NPopover } from 'naive-ui'
@@ -98,6 +105,7 @@ const useDocument = useDocumentStore()
 const popoverMode = ref(null)
 const compareStatus = computed(() => useViewer.getCompareStatus)
 const load = computed(() => useViewer.getUpload)
+const tools = computed(() => useViewer.getTools)
 let showToolMode = computed(()=>{
   const data = {
     view: $t('header.viewer'),
@@ -160,16 +168,6 @@ const openSignCreatePanel = () => {
 </script>
 
 <style lang="scss">
-  .test {
-    position: fixed;
-    right: 50px;
-    top: 50%;
-    transform: translateY(-50%);
-    padding: 20px;
-    background-color: bisque;
-    border-radius: 10px;
-    width: 200px;
-  }
   .header-items {
     display: flex;
     align-items: center;

+ 9 - 9
packages/webview/src/components/Toolbar/Toolbar.vue

@@ -1,16 +1,16 @@
 <template>
   <div class="toolbar" :class="{ hidden: toolMode === 'view' || toolMode === 'sign' || (toolMode === 'compare' && compareStatus !== 'finished'), security: toolMode === 'security'}">
-    <template v-for="(item, index) in tools[toolMode]" :key="`${item.type}-${item.dataElement || index}`">
+    <template v-for="(item, index) in toolItems[toolMode]" :key="`${item.type}-${item.dataElement || index}`">
       <!-- Annotation -->
-      <Annotate v-if="item.type === 'markup' && !item.hidden"/>
+      <Annotate v-if="item.type === 'annotation'" :item="item.tools" />
 
       <!-- Form -->
-      <TextFieldButton v-else-if="item.type === 'textFieldButton'" :item="item" />
-      <CheckBoxButton v-else-if="item.type === 'checkBoxButton'" :item="item" />
-      <RadioButton v-else-if="item.type === 'radioButton'" :item="item" />
-      <ListBox v-else-if="item.type === 'listBox'" :item="item" />
-      <ComboBox v-else-if="item.type === 'comboBox'" :item="item" />
-      <PushButton v-else-if="item.type === 'pushButton'" :item="item" />
+      <TextFieldButton v-else-if="item.type === 'textFieldButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <CheckBoxButton v-else-if="item.type === 'checkBoxButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <RadioButton v-else-if="item.type === 'radioButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <ListBox v-else-if="item.type === 'listBox' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <ComboBox v-else-if="item.type === 'comboBox' && !item.hidden" :item="item" :data-element="item.dataElement" />
+      <PushButton v-else-if="item.type === 'pushButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
 
       <!-- Security -->
       <SecuritySelect v-else-if="item.type === 'securitySelect'" :item="item" />
@@ -31,7 +31,7 @@ import { useViewerStore } from '@/stores/modules/viewer'
 defineProps(['toolMode'])
 
 const useViewer = useViewerStore()
-const tools = useViewer.getToolItems
+const toolItems = useViewer.getToolItems
 const compareStatus = computed(() => useViewer.getCompareStatus)
 </script>
 

+ 0 - 2
packages/webview/src/components/ZoomOverlay/ZoomOverlay.vue

@@ -37,7 +37,6 @@
       img="icon-header-zoom-out"
       @click="zoomOut"
       :title="$t('header.zoomOut')"
-      dataElement="zoomOutButton"
       :class="{ disabled: !load }"
     >
       <ZoomOut />
@@ -46,7 +45,6 @@
       img="icon-header-zoom-in"
       @click="zoomIn"
       :title="$t('header.zoomIn')"
-      dataElement="zoomInButton"
       :class="{ disabled: !load }"
     >
       <ZoomIn />

+ 3 - 0
packages/webview/src/main.js

@@ -8,6 +8,8 @@ import { setupStore } from '@/stores'
 
 import './assets/main.scss'
 
+import defineWebViewerInstanceUIAPIs from '@/apis'
+
 const language = window.localStorage.getItem('language') || window.navigator.language
 
 const i18n = createI18n({
@@ -25,6 +27,7 @@ const i18n = createI18n({
 const app = createApp(App)
 
 setupStore(app)
+defineWebViewerInstanceUIAPIs()
 
 app.use(i18n)
 

+ 102 - 8
packages/webview/src/stores/modules/viewer.js

@@ -101,8 +101,7 @@ export const useViewerStore = defineStore({
       {
         type: 'stickyNoteButton',
         dataElement: 'stickyNoteButton',
-        element: 'stickyNoteButton',
-        hidden: false,
+        element: 'stickyNoteButton'
       },
       {
         type: 'measureButton',
@@ -144,6 +143,11 @@ export const useViewerStore = defineStore({
         title: 'Right Panel',
         id: 'propertyPanelButton'
       },
+      {
+        type: 'pageModeButton',
+        dataElement: 'pageModeButton',
+        element: 'pageModeButton'
+      },
       {
         type: 'downloadButton',
         dataElement: 'downloadButton',
@@ -161,16 +165,103 @@ export const useViewerStore = defineStore({
         dataElement: 'printButton',
         element: 'printButton',
         title: 'Print'
+      },
+      {
+        type: 'languageButton',
+        dataElement: 'languageButton',
+        element: 'languageButton',
+        title: 'Language'
       }
     ],
     toolMode: 'view',
-    tools: {
+    tools: [
+      {
+        element: 'view',
+        dataElement: 'toolMenu-View'
+      },
+      {
+        element: 'annotation',
+        dataElement: 'toolMenu-Annotation'
+      },
+      {
+        element: 'form',
+        dataElement: 'toolMenu-Form'
+      },
+      {
+        element: 'sign',
+        dataElement: 'toolMenu-Sign'
+      },
+      {
+        element: 'security',
+        dataElement: 'toolMenu-Security'
+      },
+      {
+        element: 'compare',
+        dataElement: 'toolMenu-Compare'
+      },
+      {
+        element: 'editor',
+        dataElement: 'toolMenu-Editor'
+      }
+    ],
+    toolItems: {
       annotation: [
         {
-          type: 'markup',
-          dataElement: 'markup',
-          element: 'markup',
-          hidden: false,
+          type: 'annotation',
+          tools: [
+            {
+              type: 'note',
+              dataElement: 'note'
+            },
+            {
+              type: 'highlight',
+              dataElement: 'highlight'
+            },
+            {
+              type: 'underline',
+              dataElement: 'underline'
+            },
+            {
+              type: 'strikeout',
+              dataElement: 'strikeout'
+            },
+            {
+              type: 'squiggly',
+              dataElement: 'squiggly'
+            },
+            {
+              type: 'circle',
+              dataElement: 'circle'
+            },
+            {
+              type: 'square',
+              dataElement: 'square'
+            },
+            {
+              type: 'arrow',
+              dataElement: 'arrow'
+            },
+            {
+              type: 'line',
+              dataElement: 'line'
+            },
+            {
+              type: 'freetext',
+              dataElement: 'freetext'
+            },
+            {
+              type: 'stamp',
+              dataElement: 'stamp'
+            },
+            {
+              type: 'image',
+              dataElement: 'image'
+            },
+            {
+              type: 'link',
+              dataElement: 'link'
+            },
+          ]
         }
       ],
       form: [
@@ -303,9 +394,12 @@ export const useViewerStore = defineStore({
     getToolMode () {
       return this.toolMode
     },
-    getToolItems () {
+    getTools () {
       return this.tools
     },
+    getToolItems () {
+      return this.toolItems
+    },
     getDownloading () {
       return this.downloading
     },