使 absolute-positioned children 随其上升动态调整大小

Make absolute-positioned children dynamically resize with their ascendants

(请忽略空白方块。)

  1. 没有CSS view { height: 45em; },我得到:(位置重叠)
  2. 与 CSS view { height: 45em; },我得到:(不需要,位置不匹配)

如何在第二种情况下正确定位蓝色 <span> 元素?

<view style="height: 45em;">
  <pdf-page>                                                    <!-- position: relative -->
    <text class="textLayer">                                    <!-- position: absolute -->
      <span style="left: 417.34px; top: 37.8391px; ..."></span> <!-- position: absolute -->
    </text>
    <svg width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842" xmlns="http://www.w3.org/2000/svg" version="1.1">
      <g ⋯><g ⋯><text><tspan></tspan></text></g></g>
    </svg>
  </pdf-page>
</view>

这是 Whosebug 中的完整案例(请参阅单击“显示代码片段”后第二个窗格中的 /* ← */):

@namespace     url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

/*pdf.css*/
:root {
  --pdf-page-outline-color: #aaa;
  --pdf-page-background-color: #fcfcfc;
}

pdf-file { display: contents; }
pdf-page {
  display: inline-block;
  outline: 1px solid var(--pdf-page-outline-color);
  background-color:  var(--pdf-page-background-color);
}

pdf-page { position: relative; }

/* text.css */
.textLayer {
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  width: 100%; height: 100%;
 -overflow: hidden;
  opacity: 1;
 -line-height: 1;
}

.textLayer > span {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  -webkit-transform-origin: 0% 0%;
          transform-origin: 0% 0%;
}

/**/
 view      { background: green; }
.textLayer { background: rgba(0, 255, 0, .1); }
 svg|svg   { background: rgba(255, 0, 0, .1); }
<style>
  view {
    height: 45em; /* ← */
    display: flex;
    overflow: auto;
    flex-direction: column;
    place-items: center;
    scroll-snap-type: y mandatory;
    overflow: auto;
  }

  pdf-page { height: 100%; scroll-snap-align: start; }
  svg { height: 100%; width: auto; }

  text { overflow: visible; background: rgb(0, 0, 0, .1); }
  text > span { background: rgba(0,0,255,.1); }
</style>

<view -onclick="this.requestFullscreen()">
  <pdf-page of="f" no="+1" svg="">
    <text class="textLayer">
      <span style="left: 417.34px; top: 37.8391px; font-size: 12px; font-family: sans-serif; transform: scaleX(1.07482);">Plenarprotokoll 16/3</span>
    </text>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842">
      <g transform="matrix(1 0 0 -1 -8 850)">
        <g transform="">
          <text transform="matrix(12 0 0 12 425.34 801.2976) scale(1, -1)" xml:space="preserve">
            <tspan x="0 0.6672 0.9454 1.5016 2.1128 2.669 3.0582 3.6694 4.0586 4.6698 5.003 5.6142 6.1704 6.7816 7.0598 7.6132 8.1694 8.7256 9.0038" y="0" font-family="g_d0_f1" font-size="1px" fill="rgb(0,0,0)"></tspan>
          </text>
        </g>
      </g>
    </svg>
  </pdf-page>
</view>

(也可在 codepen 上查看:https://codepen.io/cetinsert/pen/MWeVxLe?editors=1100

给定视口 widthheight,从 <span style> 像素 [=] 进行 one-time 转换 48=]到百分比:

const px2pc = ({ width, height }) => s => {
  const      c = s.style;
  const l = +c.getPropertyValue('left'     ).slice(0, -2); // drop px
  const t = +c.getPropertyValue('top'      ).slice(0, -2);
  const f = +c.getPropertyValue('font-size').slice(0, -2);
             c.setProperty     ('left',      `${(l / width)  * 100}%`);
             c.setProperty     ('top',       `${(t / height) * 100}%`);
             c.setProperty     ('font-size', `${(f / height) * 100}%`);
};

并在 <text> 元素的祖先引起调整大小时考虑字体大小调整:

const t = document.querySelector('text');
const r = new ResizeObserver(textFontResize(t));
      r.observe(t);
const textFontResize = t => ([ a ]) => {
  const i = t.parentNode.lastElementChild; // <svg> | <canvas>
            t.style.setProperty('font-size', `${i.clientHeight}px`);
};

证明自己是一个非常强大且相对简单的解决方案。

(如果有人想出更优雅的方式,请不要诉诸 ResizeObserver,请 post 一个新的答案。)


演示

(对于这个问题,外部资产是 version-fixed。)

  1. 滚动到此答案的末尾
  2. 点击▶️运行代码段
  3. 命中⤤ Full page

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.min.js" integrity="sha512-Z8CqofpIcnJN80feS2uccz+pXWgZzeKxDsDNMD/dJ6997/LSRY+W4NmEt9acwR+Gt9OHN0kkI1CTianCwoqcjQ==" crossorigin="anonymous"></script>
    <script src="//shin.glitch.me/shin.q1.js"></script>
    <script src="//shin.glitch.me/pdf.q1.js"></script>
    
    <!-- private resources -->
    <link  href="//cdn.blue/{fa-5.15}/css/all.css" rel="stylesheet">
    <link  href="//cdn.blue/{fa+}/var.css"         rel="stylesheet">
    <link  href="//cdn.blue/{fa+}/x.css"           rel="stylesheet">
    <!-- private resources -->
    
    <style>:root { max-width: 50em; margin: auto; }</style>
    
    <script>console.clear();</script>
    <style>html, body { padding: 0; margin: 0; font-family: system-ui; }</style>
    
    <script>
class CodeEditElement extends ShinElement {
  constructor() {
    super(`
<style>:host { display: block; overflow: hidden; } pre { height: 100%; margin: 0; }</style>
<pre contenteditable spellcheck=false inputmode=text></pre>`, { _: { QS: { T: [ 'pre' ] } } });
    const e = this;
          e.ph = v => { const e = v.target; if (!e.childElementCount) return; e.textContent = e.textContent; };
  }
     connectedCallback() { this._.pre.   addEventListener('input', this.ph); }
  disconnectedCallback() { this._.pre.removeEventListener('input', this.ph); }
  get textContent()  { return this._.pre.textContent;     }
  set textContent(v) {        this._.pre.textContent = v; }
}
CodeEditElement.define();
      
class CodeLiveElement extends ShinElement {
      constructor()  { super(`<live></live>`, { _: { QS: { T: [ 'live' ] } } }); }
  get textContent()  { return this._.live.textContent; }
  set textContent(v) {        this._.live.textContent = v; }
  get innerHTML()    { return this._.live.innerHTML; }
  set innerHTML(v)   {        this._.live.innerHTML = v; this.evalScripts(); }
      evalScripts()  { this._.QA('script').forEach(s => eval(s.textContent)); }
}
CodeLiveElement.define();
            
class CodePlayElement extends ShinElement {
  constructor() {
    super(`
      <style>
        :host(:not([open])) > code-edit { display: none; }
        :host > div       { display: flex; justify-content: stretch; align-items: stretch; }
        ::slotted(select) { flex: 1; }
        * { border-color: var(--bd); }
      </style>
      <div part=controls>
        <slot></slot>
        <button id=reset><slot name=reset></slot></button>
        <button id=open><slot name=open></slot></button>
      </div>
      <code-edit id=pre part=edit></code-edit>`, 
      { _: { QS: { S: { '#pre': 'pre', '#reset': 'reset', '#open': 'open' } } } }
    );
    const e = this;
          e.sc = v => { const tx = e.tx;             e.px = tx; };
          e.pi = v => {                   e.t.ux = e.px;      };
          e.rc = v => {                              e.tr();             };
          e.oc = v => {                              e.open =!e.open;    };
    Shin.IPA(e, 'open', { t: Shin.Boolean });
  }
     connectedCallback() { setTimeout(() => this._init()); }
  disconnectedCallback() {                  this._dest();  }
  static cleanCode(t = "") {
    return t.trim().replace(/^[\n]+/g, '').replace(/\s+$/g, '').split('\n').map(l => l.trimEnd()).join('\n');
  }
  get s()   { return    this.QS('select'); }
  get S()   { const o = this.QA('option'); return o.filter(o => o.selected); }
  get t()   { return [].concat(...this.S.map(o => Shin.QA('template', o))); } 
  get tx()  { return    this.t.map(t => t.ux || this.constructor.cleanCode(t.innerHTML)).join('\n'); }
      tr()  {           this.t.ux = undefined; this.sc(); }
  get r()   { return    this._.reset; }
  get o()   { return    this._.open; }
  get p()   { return    this._.pre; }
  get P()   { return    this._.QA('pre'); }
  get px()  { return    this.p.textContent; }
  set px(v) {           this.p.textContent = v; this.oninput(); }
  _init() { 
    const e = this;                      e.sc();
          e.s.addEventListener('change', e.sc);
          e.p.addEventListener('input',  e.pi);
          e.r.addEventListener('click',  e.rc);
          e.o.addEventListener('click',  e.oc);
  }
  _dest() { 
    const e = this;
          e.s.removeEventListener('change', e.sc);
          e.p.removeEventListener('input',  e.pi);
          e.p.removeEventListener('click',  e.rc);
          e.p.removeEventListener('click',  e.oc); 
  }
}
CodePlayElement.define();
    </script>
    <style>
      body { padding: 1em; overflow: scroll; font-family: system-ui; }
      :root {
        --list-bg: #eee;        
        --code-bg: #fefefe;
        --live-bg_: #ccc;
        --bd: #ccc;
      }
      code-play    { display: flex; width: 100%; flex-direction: row-reverse; }
      code-play:not(:first-of-type) { margin-top: 1em; }
      ::part(edit)  { min-height: 1em; min-width: 1em; overflow-x: auto; background-color: var(--code-bg); }

      x[undo]:before, x[undo]:after { content: var(--fa-undo); }
      x[open]:before, x[open]:after { content: var(--fa-eye-slash); }
       [open]         x[open]:before,
       [open]         x[open]:after { content: var(--fa-eye); }

      select { background: var(--list-bg); border-color: var(--bd); overflow: auto; }
      live   { background: var(--live-bg); display: block; bordxer: 1px solid var(--bd); }
      
      code-play:not([open]) + live { _display: none; }
      ::part(edit) { border: 1px solid var(--bd); flex: 1; }
      ::part(controls) { flex-direction: column-reverse; }
      ::part() { border-radius: 3px; }
    </style>
    <style>
      code-play:not([open]) { height: 2.7em; _outline: 1px solid; }
      code-play:not([open]) > select { display: none; }
    </style>
  </head>
  <body>

    <code-live id="cl"></code-live><script>cl.innerHTML = "";</script>
    
    <script>
      const pes = 'previousElementSibling';
      const n =  (p, N = 1) => e => { let j = e[p]; for (let i = 1; i < N; i++) j = j[p]; return j; };
      const c = n(pes, 2);
      const l = n(pes, 1);
      const _ = () => document.currentScript;
    </script>
            
    <code-play open>
      <select multiple size="1">
        <option selected>file<template>
<pdf-file id="f" src="//pdf.systems/16003.pdf"></pdf-file>
<pdf-file id="g" src="//pdf.systems/16004.pdf"></pdf-file>
        </template></option>
      </select>
      <x open slot="open"></x>
      <x undo slot="reset"></x>
    </code-play>
    <live></live>
    <script>{
      const _ = document.currentScript;
          c(_).oninput = () => 
          l(_).innerHTML = c(_).px;
    }
    </script>
    
    <code-play open style="min-height: 11em;">
      
      <select multiple size="6">

        <optgroup label="File Reference">

          <option>by attribute<!-- !!!!!!!!! --><template>
<pdf-page of="f" no="+1" scale=".1"></pdf-page>
<pdf-page of="f" no="+1" scale=".2"></pdf-page>
<pdf-page of="f" no="+1" scale=".3"></pdf-page>
<pdf-page of="f" no="+1" scale=".4"></pdf-page>
<pdf-page of="f" no="+1" scale=".5"></pdf-page>
<pdf-page of="f" no="+1" scale=".5" svg=""></pdf-page>
          </template></option> 

          <option>by ancestry<!-- !!!!!!!!! --><template>
<pdf-file src="//pdf.systems/16008.pdf">
  <pdf-page no="+1" scale=".4" svg></pdf-page>
  <pdf-page no="+3" scale=".4" svg></pdf-page>
  <pdf-page no="-1" scale=".4" svg></pdf-page>
</pdf-file>
          </template></option>

        </optgroup>
        
        <optgroup label="Embed Mode">
        
          <option selected>Sized Container ⭤<!-- !!!!!!!!! --><template>
<style>
  view { width: 10em; height: 25em; /* ← */
    display: block; background: white; overflow: auto;
  }

  pdf-page        { width: 100%; }
    ::part(layer) { width: 100%; height: auto; }
</style>

<view onclick="this.requestFullscreen()">

  <pdf-page of="f" no="+1" xvg="" scale=".2"></pdf-page>
  <pdf-page of="f" no="+1" xvg="" scale="1"></pdf-page>
  <pdf-page of="f" no="+1" xvg="" scale="2"></pdf-page>
  <pdf-page of="f" no="+1" svg=""></pdf-page>
  
  <pdf-page of="f" no="-1" xvg=""></pdf-page>
  <pdf-page of="g" no="-1" svg=""></pdf-page>

</view>
            </template></option>
          
        </optgroup>
        
      </select>
      
      <x open slot="open"></x>
      <x undo slot="reset"></x>
      
    </code-play>
    <live></live>
    <script>{ const _ = document.currentScript; c(_).oninput = () => l(_).innerHTML = c(_).px; }</script>

    <style>live { display: flex; align-items: flex-end; flex-wrap: wrap; } pdf-file { display: contents; }</style>    

    <h3>Styling</h3>
    <p>Styles can be easily applied. (Try <strong><kbd>Ctrl</kbd></strong> + <i class="fa fa-mouse-pointer"></i> to unselect / select multiple.)</p>
    <code-play open>
      <select multiple size="8">
        <optgroup label="Page">
          <option>outline   <template><style>pdf-page { outline: 1px dotted; }</style></template></option>
          <option>background<template><style>pdf-page { background-color: rgb(200, 200, 255, .1); }</style></template></option>
        </optgroup>
        <optgroup label="Text">
          <option selected>mark<template><style>::part(span) { background-color: rgb(255, 0, 0, .1); }</style></template></option>
        </optgroup>
        <optgroup label="Image">
          <option>hidden     <template><style>::part(image) { opacity: 0; }</style></template></option>
          <option>pixelated  <template><style>::part(image) { image-rendering: pixelated; }</style></template></option>
          <option>crisp-edges<template><style>::part(image) { image-rendering: crisp-edges; }</style></template></option>
        </optgroup>
      </select>
      <x open slot="open"></x>
      <x undo slot="reset"></x>
    </code-play>
    <live></live>
    <script>{ const _ = document.currentScript; c(_).oninput = () => l(_).innerHTML = c(_).px; }</script>
    
    <script>
     document.addEventListener(  'load', e => console.warn('l', e.target));
     document.addEventListener('unload', e => console.warn('u', e.target));
    </script>
    <p style="margin-bottom: 10em;"><a href="https://shin.glitch.me/pa.html">Documentation (WIP)</a></p>
            
  </body>
</html>

确实不可能通过 CSS 以干净的方式完成。例如,这会起作用,但是因为您将跨度定位在图片顶部,所以所有数字都是硬编码的:

.textLayer > span{
    right: 10% !important;
    left: auto !important;
    top: 0 !important;
    margin-top: 6%;/*margin-top uses 6% of the WIDTH, not 6% of the height. It's very useful when trying to place something on top of an image.*/
    width: 20%;
    height: 2%;
}

这是您的代码段的复制品,其中添加了 CSS:

@namespace     url(http://www.w3.org/1999/xhtml);
@namespace svg url(http://www.w3.org/2000/svg);

/*pdf.css*/
:root {
  --pdf-page-outline-color: #aaa;
  --pdf-page-background-color: #fcfcfc;
}

pdf-file { display: contents; }
pdf-page {
  display: inline-block;
  outline: 1px solid var(--pdf-page-outline-color);
  background-color:  var(--pdf-page-background-color);
}

pdf-page { position: relative; }

/* text.css */
.textLayer {
  position: absolute;
  left: 0; top: 0; right: 0; bottom: 0;
  width: 100%; height: 100%;
 -overflow: hidden;
  opacity: 1;
  line-height: 1;
}

.textLayer > span {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  -webkit-transform-origin: 0% 0%;
          transform-origin: 0% 0%;
}
.textLayer > span{
    right: 10% !important;
    left: auto !important;
    top: 0 !important;
    margin-top: 6%;/*margin-top uses 6% of the WIDTH, not the height. It's sometimes more useful than ordinary top:6%.*/
    width: 20%;
    height: 2%;
}
/**/
 view      { background: green; }
.textLayer { background: rgba(0, 255, 0, .1); }
 svg|svg   { background: rgba(255, 0, 0, .1); }
<style>
  view {
    height: 45em; /* ← */
    display: flex;
    overflow: auto;
    flex-direction: column;
    place-items: center;
    scroll-snap-type: y mandatory;
    overflow: auto;
  }

  pdf-page { height: 100%; scroll-snap-align: start; }
  svg { height: 100%; width: auto; }

  text { overflow: visible; background: rgb(0, 0, 0, .1); }
  text > span { background: rgba(0,0,255,.1); }
</style>

<view -onclick="this.requestFullscreen()">
  <pdf-page of="f" no="+1" svg="">
    <text class="textLayer">
      <span style="left: 417.34px; top: 37.8391px; font-size: 12px; font-family: sans-serif; transform: scaleX(1.07482);">Plenarprotokoll 16/3</span>
    </text>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="595px" height="842px" preserveAspectRatio="none" viewBox="0 0 595 842">
      <g transform="matrix(1 0 0 -1 -8 850)">
        <g transform="">
          <text transform="matrix(12 0 0 12 425.34 801.2976) scale(1, -1)" xml:space="preserve">
            <tspan x="0 0.6672 0.9454 1.5016 2.1128 2.669 3.0582 3.6694 4.0586 4.6698 5.003 5.6142 6.1704 6.7816 7.0598 7.6132 8.1694 8.7256 9.0038" y="0" font-family="g_d0_f1" font-size="1px" fill="rgb(0,0,0)"></tspan>
          </text>
        </g>
      </g>
    </svg>
  </pdf-page>
</view>

编辑:更清楚一点。

我们想把方框放在文本的上面。用于文本位置和 width/height 的数字可能看起来是任意的,但这仅仅是因为我们试图覆盖的项目的位置也具有任意位置 /width/height。 (如果你愿意,我们可以谈谈如何使用 GIMP 检查你的图像的纵横比,但是..

一个。我认为使用 GIMP 测量正确的值不在这个答案的范围内(您可以通过计算图像的宽度和图像的高度来找到纵横比,然后使用该纵横比来计算它使用起点的 X/Y 坐标和终点的 X/Y 坐标来计算您需要使用的百分比....但是,好吧....)

b。通常 显着 比 fiddle 在 Chrome 的开发工具中使用它 15 分钟要快,

作为一般规则,当使用 position: absolute 将某些内容放在图像之上时,您的代码将如下所示:

.item{
    position:absolute;
    top:0;
    margin-top:W%; //The reason we use margin instead of top is because margin is based off width, which allows us to maintain aspect ratio on our positioning.
    left:X%; // Or right
    width:Y%;
    height:Z%;
}

编辑2:我本来用的是vwvh,对于这种定位往往非常有用,但最后还是可以重构出来,就是为什么我们使用的唯一 non-standard 定位是 margin-top.

一种更精确的方法是 transform: scale(x, y) <text> 层一次调整大小,没有任何 <span style> 位置值重新计算/单位更改。


这个回答触发了我的商业项目的启动。

https://WebPDF.pro

Zero-dependency,真正的 HTML-native PDF web 组件。


const t = document.querySelector('text');
const r = new ResizeObserver(textResize(t));
      r.observe(t);
const textResize  = t => ([ a ]) => {
  const         e = t.parentNode.lastElementChild; // <svg> | <canvas>
  const         i = PDFPageElement.image(e);       // { height, width };
  const     h = e.clientHeight;
  const x = h / i.      height;
  const     w = e.clientWidth;
  const y = w / i.      width;
                    t.style.setProperty('transform', `scale(${x}, ${y})`);
};
PDFPageElement.image = i => { if (!i) return;
  switch (i.tagName) {
    case 'CANVAS':   return { height: i.height,               width: i.width               };
    default: /*SVG*/ return { height: i.height.baseVal.value, width: i.width.baseVal.value };
  }    
};

有 1 个额外的 CSS 规则

.textLayer { overflow: visible; }

之前/之后