diff --git a/src/core/annotation.js b/src/core/annotation.js index f09affc3edfd54..7c941009467b41 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -52,8 +52,6 @@ var ColorSpace = coreColorSpace.ColorSpace; var ObjectLoader = coreObj.ObjectLoader; var OperatorList = coreEvaluator.OperatorList; -var DEFAULT_ICON_SIZE = 22; // px - /** * @class * @alias AnnotationFactory @@ -95,6 +93,9 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { } return new WidgetAnnotation(parameters); + case 'Popup': + return new PopupAnnotation(parameters); + default: warn('Unimplemented annotation type "' + subtype + '", ' + 'falling back to base annotation'); @@ -160,7 +161,7 @@ var Annotation = (function AnnotationClosure() { // Expose public properties using a data object. this.data = {}; - this.data.id = params.ref.num; + this.data.id = params.ref.toString(); this.data.subtype = dict.get('Subtype').name; this.data.annotationFlags = this.flags; this.data.rect = this.rectangle; @@ -639,29 +640,35 @@ var TextWidgetAnnotation = (function TextWidgetAnnotationClosure() { })(); var TextAnnotation = (function TextAnnotationClosure() { - function TextAnnotation(params) { - Annotation.call(this, params); + var DEFAULT_ICON_SIZE = 22; // px - var dict = params.dict; - var data = this.data; + function TextAnnotation(parameters) { + Annotation.call(this, parameters); - var content = dict.get('Contents'); - var title = dict.get('T'); - data.annotationType = AnnotationType.TEXT; - data.content = stringToPDFString(content || ''); - data.title = stringToPDFString(title || ''); - data.hasHtml = true; + this.data.annotationType = AnnotationType.TEXT; + this.data.hasHtml = true; - if (data.hasAppearance) { - data.name = 'NoIcon'; + var dict = parameters.dict; + if (this.data.hasAppearance) { + this.data.name = 'NoIcon'; } else { - data.rect[1] = data.rect[3] - DEFAULT_ICON_SIZE; - data.rect[2] = data.rect[0] + DEFAULT_ICON_SIZE; - data.name = dict.has('Name') ? dict.get('Name').name : 'Note'; + this.data.rect[1] = this.data.rect[3] - DEFAULT_ICON_SIZE; + this.data.rect[2] = this.data.rect[0] + DEFAULT_ICON_SIZE; + this.data.name = dict.has('Name') ? dict.get('Name').name : 'Note'; } - if (dict.has('C')) { - data.hasBgColor = true; + if (!dict.has('C')) { + // Fall back to the default background color. + this.data.color = null; + } + + this.data.hasPopup = dict.has('Popup'); + if (!this.data.hasPopup) { + // There is no associated Popup annotation, so the Text annotation + // must create its own popup. + this.data.title = stringToPDFString(dict.get('T') || ''); + this.data.contents = stringToPDFString(dict.get('Contents') || ''); + this.data.hasHtml = (this.data.title || this.data.contents); } } @@ -746,6 +753,39 @@ var LinkAnnotation = (function LinkAnnotationClosure() { return LinkAnnotation; })(); +var PopupAnnotation = (function PopupAnnotationClosure() { + function PopupAnnotation(parameters) { + Annotation.call(this, parameters); + + this.data.annotationType = AnnotationType.POPUP; + + var dict = parameters.dict; + var parentItem = dict.get('Parent'); + if (!parentItem) { + warn('Popup annotation has a missing or invalid parent annotation.'); + return; + } + + this.data.parentId = dict.getRaw('Parent').toString(); + this.data.title = stringToPDFString(parentItem.get('T') || ''); + this.data.contents = stringToPDFString(parentItem.get('Contents') || ''); + + if (!parentItem.has('C')) { + // Fall back to the default background color. + this.data.color = null; + } else { + this.setColor(parentItem.get('C')); + this.data.color = this.color; + } + + this.data.hasHtml = (this.data.title || this.data.contents); + } + + Util.inherit(PopupAnnotation, Annotation, {}); + + return PopupAnnotation; +})(); + exports.Annotation = Annotation; exports.AnnotationBorderStyle = AnnotationBorderStyle; exports.AnnotationFactory = AnnotationFactory; diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 65d337dd50b5cd..9a1545d816637d 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -36,11 +36,10 @@ var LinkTargetStringMap = sharedUtil.LinkTargetStringMap; var warn = sharedUtil.warn; var CustomStyle = displayDOMUtils.CustomStyle; -var ANNOT_MIN_SIZE = 10; // px - /** * @typedef {Object} AnnotationElementParameters * @property {Object} data + * @property {HTMLDivElement} layer * @property {PDFPage} page * @property {PageViewport} viewport * @property {IPDFLinkService} linkService @@ -70,6 +69,9 @@ AnnotationElementFactory.prototype = case AnnotationType.WIDGET: return new WidgetAnnotationElement(parameters); + case AnnotationType.POPUP: + return new PopupAnnotationElement(parameters); + default: throw new Error('Unimplemented annotation type "' + subtype + '"'); } @@ -83,6 +85,7 @@ AnnotationElementFactory.prototype = var AnnotationElement = (function AnnotationElementClosure() { function AnnotationElement(parameters) { this.data = parameters.data; + this.layer = parameters.layer; this.page = parameters.page; this.viewport = parameters.viewport; this.linkService = parameters.linkService; @@ -292,8 +295,6 @@ var LinkAnnotationElement = (function LinkAnnotationElementClosure() { var TextAnnotationElement = (function TextAnnotationElementClosure() { function TextAnnotationElement(parameters) { AnnotationElement.call(this, parameters); - - this.pinned = false; } Util.inherit(TextAnnotationElement, AnnotationElement, { @@ -305,127 +306,35 @@ var TextAnnotationElement = (function TextAnnotationElementClosure() { * @returns {HTMLSectionElement} */ render: function TextAnnotationElement_render() { - var rect = this.data.rect, container = this.container; - - // Sanity check because of OOo-generated PDFs. - if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) { - rect[3] = rect[1] + ANNOT_MIN_SIZE; - } - if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) { - rect[2] = rect[0] + (rect[3] - rect[1]); // make it square - } - - container.className = 'annotText'; + this.container.className = 'textAnnotation'; - var image = document.createElement('img'); - image.style.height = container.style.height; - image.style.width = container.style.width; - var iconName = this.data.name; + var image = document.createElement('img'); + image.style.height = this.container.style.height; + image.style.width = this.container.style.width; image.src = PDFJS.imageResourcesPath + 'annotation-' + - iconName.toLowerCase() + '.svg'; + this.data.name.toLowerCase() + '.svg'; image.alt = '[{{type}} Annotation]'; image.dataset.l10nId = 'text_annotation_type'; - image.dataset.l10nArgs = JSON.stringify({type: iconName}); - - var contentWrapper = document.createElement('div'); - contentWrapper.className = 'annotTextContentWrapper'; - contentWrapper.style.left = Math.floor(rect[2] - rect[0] + 5) + 'px'; - contentWrapper.style.top = '-10px'; - - var content = this.content = document.createElement('div'); - content.className = 'annotTextContent'; - content.setAttribute('hidden', true); - - var i, ii; - if (this.data.hasBgColor && this.data.color) { - var color = this.data.color; - - // Enlighten the color (70%). - var BACKGROUND_ENLIGHT = 0.7; - var r = BACKGROUND_ENLIGHT * (255 - color[0]) + color[0]; - var g = BACKGROUND_ENLIGHT * (255 - color[1]) + color[1]; - var b = BACKGROUND_ENLIGHT * (255 - color[2]) + color[2]; - content.style.backgroundColor = Util.makeCssRgb(r | 0, g | 0, b | 0); - } - - var title = document.createElement('h1'); - var text = document.createElement('p'); - title.textContent = this.data.title; - - if (!this.data.content && !this.data.title) { - content.setAttribute('hidden', true); - } else { - var e = document.createElement('span'); - var lines = this.data.content.split(/(?:\r\n?|\n)/); - for (i = 0, ii = lines.length; i < ii; ++i) { - var line = lines[i]; - e.appendChild(document.createTextNode(line)); - if (i < (ii - 1)) { - e.appendChild(document.createElement('br')); - } - } - text.appendChild(e); - - image.addEventListener('click', this._toggle.bind(this)); - image.addEventListener('mouseover', this._show.bind(this, false)); - image.addEventListener('mouseout', this._hide.bind(this, false)); - content.addEventListener('click', this._hide.bind(this, true)); - } - - content.appendChild(title); - content.appendChild(text); - contentWrapper.appendChild(content); - container.appendChild(image); - container.appendChild(contentWrapper); - return container; - }, - - /** - * Toggle the visibility of the content box. - * - * @private - * @memberof TextAnnotationElement - */ - _toggle: function TextAnnotationElement_toggle() { - if (this.pinned) { - this._hide(true); - } else { - this._show(true); - } - }, - - /** - * Show the content box. - * - * @private - * @param {boolean} pin - * @memberof TextAnnotationElement - */ - _show: function TextAnnotationElement_show(pin) { - if (pin) { - this.pinned = true; + image.dataset.l10nArgs = JSON.stringify({type: this.data.name}); + + if (!this.data.hasPopup) { + var popupElement = new PopupElement({ + parentContainer: this.container, + parentTrigger: image, + color: this.data.color, + title: this.data.title, + contents: this.data.contents + }); + var popup = popupElement.render(); + + // Position the popup next to the Text annotation's container. + popup.style.left = image.style.width; + + this.container.appendChild(popup); } - if (this.content.hasAttribute('hidden')) { - this.container.style.zIndex += 1; - this.content.removeAttribute('hidden'); - } - }, - /** - * Hide the content box. - * - * @private - * @param {boolean} unpin - * @memberof TextAnnotationElement - */ - _hide: function TextAnnotationElement_hide(unpin) { - if (unpin) { - this.pinned = false; - } - if (!this.content.hasAttribute('hidden') && !this.pinned) { - this.container.style.zIndex -= 1; - this.content.setAttribute('hidden', true); - } + this.container.appendChild(image); + return this.container; } }); @@ -499,6 +408,189 @@ var WidgetAnnotationElement = (function WidgetAnnotationElementClosure() { return WidgetAnnotationElement; })(); +/** + * @class + * @alias PopupAnnotationElement + */ +var PopupAnnotationElement = (function PopupAnnotationElementClosure() { + function PopupAnnotationElement(parameters) { + AnnotationElement.call(this, parameters); + } + + Util.inherit(PopupAnnotationElement, AnnotationElement, { + /** + * Render the popup annotation's HTML element in the empty container. + * + * @public + * @memberof PopupAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function PopupAnnotationElement_render() { + this.container.className = 'popupAnnotation'; + + var selector = '[data-annotation-id="' + this.data.parentId + '"]'; + var parentElement = this.layer.querySelector(selector); + if (!parentElement) { + return this.container; + } + + var popup = new PopupElement({ + parentContainer: parentElement, + parentTrigger: parentElement, + color: this.data.color, + title: this.data.title, + contents: this.data.contents + }); + + // Position the popup next to the parent annotation's container. + // PDF viewers ignore a popup annotation's rectangle. + var parentLeft = parseFloat(parentElement.style.left); + var parentWidth = parseFloat(parentElement.style.width); + CustomStyle.setProp('transformOrigin', this.container, + -(parentLeft + parentWidth) + 'px -' + + parentElement.style.top); + this.container.style.left = (parentLeft + parentWidth) + 'px'; + + this.container.appendChild(popup.render()); + return this.container; + } + }); + + return PopupAnnotationElement; +})(); + +/** + * @class + * @alias PopupElement + */ +var PopupElement = (function PopupElementClosure() { + var BACKGROUND_ENLIGHT = 0.7; + + function PopupElement(parameters) { + this.parentContainer = parameters.parentContainer; + this.parentTrigger = parameters.parentTrigger; + this.color = parameters.color; + this.title = parameters.title; + this.contents = parameters.contents; + + this.pinned = false; + } + + PopupElement.prototype = /** @lends PopupElement.prototype */ { + /** + * Render the popup's HTML element. + * + * @public + * @memberof PopupElement + * @returns {HTMLSectionElement} + */ + render: function PopupElement_render() { + var wrapper = document.createElement('div'); + wrapper.className = 'popupWrapper'; + + var popup = this.popup = document.createElement('div'); + popup.className = 'popup'; + popup.setAttribute('hidden', true); + + var color = this.color; + if (color) { + // Enlighten the color. + var r = BACKGROUND_ENLIGHT * (255 - color[0]) + color[0]; + var g = BACKGROUND_ENLIGHT * (255 - color[1]) + color[1]; + var b = BACKGROUND_ENLIGHT * (255 - color[2]) + color[2]; + popup.style.backgroundColor = Util.makeCssRgb(r | 0, g | 0, b | 0); + } + + var contents = this._formatContents(this.contents); + var title = document.createElement('h1'); + title.textContent = this.title; + + // Attach the event listeners to the trigger element. + var trigger = this.parentTrigger; + trigger.addEventListener('click', this._toggle.bind(this)); + trigger.addEventListener('mouseover', this._show.bind(this, false)); + trigger.addEventListener('mouseout', this._hide.bind(this, false)); + popup.addEventListener('click', this._hide.bind(this, true)); + + popup.appendChild(title); + popup.appendChild(contents); + wrapper.appendChild(popup); + return wrapper; + }, + + /** + * Format the contents of the popup by adding newlines where necessary. + * + * @private + * @param {string} contents + * @memberof PopupElement + * @returns {HTMLParagraphElement} + */ + _formatContents: function PopupElement_formatContents(contents) { + var p = document.createElement('p'); + var lines = contents.split(/(?:\r\n?|\n)/); + for (var i = 0, ii = lines.length; i < ii; ++i) { + var line = lines[i]; + p.appendChild(document.createTextNode(line)); + if (i < (ii - 1)) { + p.appendChild(document.createElement('br')); + } + } + return p; + }, + + /** + * Toggle the visibility of the popup. + * + * @private + * @memberof PopupElement + */ + _toggle: function PopupElement_toggle() { + if (this.pinned) { + this._hide(true); + } else { + this._show(true); + } + }, + + /** + * Show the popup. + * + * @private + * @param {boolean} pin + * @memberof PopupElement + */ + _show: function PopupElement_show(pin) { + if (pin) { + this.pinned = true; + } + if (this.popup.hasAttribute('hidden')) { + this.popup.removeAttribute('hidden'); + this.parentContainer.style.zIndex += 1; + } + }, + + /** + * Hide the popup. + * + * @private + * @param {boolean} unpin + * @memberof PopupElement + */ + _hide: function PopupElement_hide(unpin) { + if (unpin) { + this.pinned = false; + } + if (!this.popup.hasAttribute('hidden') && !this.pinned) { + this.popup.setAttribute('hidden', true); + this.parentContainer.style.zIndex -= 1; + } + } + }; + + return PopupElement; +})(); + /** * @typedef {Object} AnnotationLayerParameters * @property {PageViewport} viewport @@ -532,6 +624,7 @@ var AnnotationLayer = (function AnnotationLayerClosure() { var properties = { data: data, + layer: parameters.div, page: parameters.page, viewport: parameters.viewport, linkService: parameters.linkService diff --git a/test/annotation_layer_test.css b/test/annotation_layer_test.css index 3101be4cd01ea2..5c0afc0b6ff678 100644 --- a/test/annotation_layer_test.css +++ b/test/annotation_layer_test.css @@ -39,35 +39,33 @@ box-shadow: 0px 2px 10px #ff0; } -.annotationLayer .annotText > img { - position: absolute; -} - -.annotationLayer .annotTextContentWrapper { +.annotationLayer .popupWrapper { position: absolute; width: 20em; } -.annotationLayer .annotTextContent { +.annotationLayer .popup { + position: absolute; z-index: 200; - float: left; max-width: 20em; background-color: #FFFF99; box-shadow: 0px 2px 5px #333; border-radius: 2px; padding: 0.6em; + margin-left: 5px; display: block !important; font: message-box; + word-wrap: break-word; } -.annotationLayer .annotTextContent > h1 { +.annotationLayer .popup h1 { font-size: 1em; border-bottom: 1px solid #000000; margin: 0; padding: 0 0 0.2em 0; } -.annotationLayer .annotTextContent > p { +.annotationLayer .popup p { margin: 0; padding: 0.2em 0 0 0; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index c2a6733466ccb0..d3a76808551200 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -198,3 +198,5 @@ !issue6108.pdf !issue6113.pdf !openoffice.pdf +!annotation-link-text-popup.pdf +!annotation-text-without-popup.pdf diff --git a/test/pdfs/annotation-link-text-popup.pdf b/test/pdfs/annotation-link-text-popup.pdf new file mode 100644 index 00000000000000..2a840b172905be Binary files /dev/null and b/test/pdfs/annotation-link-text-popup.pdf differ diff --git a/test/pdfs/annotation-text-without-popup.pdf b/test/pdfs/annotation-text-without-popup.pdf new file mode 100644 index 00000000000000..5c02df1df924c7 Binary files /dev/null and b/test/pdfs/annotation-text-without-popup.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 0538c17284e38c..92d433df11e6b0 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2616,6 +2616,21 @@ "rounds": 1, "type": "load" }, + { "id": "annotation-link-text-popup", + "file": "pdfs/annotation-link-text-popup.pdf", + "md5": "4bbf56e81d47232de5f305124ab0ba27", + "rounds": 1, + "type": "eq", + "annotations": true + }, + { "id": "annotation-text-without-popup", + "file": "pdfs/annotation-text-without-popup.pdf", + "md5": "7c2d241babe00139e34b9f8369a909eb", + "rounds": 1, + "type": "eq", + "annotations": true, + "about": "Text annotation without a separate Popup annotation" + }, { "id": "issue6108", "file": "pdfs/issue6108.pdf", "md5": "8961cb55149495989a80bf0487e0f076", diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index dc5a161c93d235..f3196a318ec462 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -23,34 +23,35 @@ box-shadow: 0px 2px 10px #ff0; } -.annotationLayer .annotText > img { - position: absolute; +.annotationLayer .textAnnotation { cursor: pointer; } -.annotationLayer .annotTextContentWrapper { +.annotationLayer .popupWrapper { position: absolute; width: 20em; } -.annotationLayer .annotTextContent { +.annotationLayer .popup { + position: absolute; z-index: 200; - float: left; max-width: 20em; background-color: #FFFF99; box-shadow: 0px 2px 5px #333; border-radius: 2px; padding: 0.6em; + margin-left: 5px; cursor: pointer; + word-wrap: break-word; } -.annotationLayer .annotTextContent > h1 { +.annotationLayer .popup h1 { font-size: 1em; border-bottom: 1px solid #000000; padding-bottom: 0.2em; } -.annotationLayer .annotTextContent > p { +.annotationLayer .popup p { padding-top: 0.2em; }